@faststore/core 3.60.3 → 3.61.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 (66) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +54 -54
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/server-production/0.pack +0 -0
  8. package/.next/cache/webpack/server-production/index.pack +0 -0
  9. package/.next/prerender-manifest.js +1 -1
  10. package/.next/prerender-manifest.json +1 -1
  11. package/.next/react-loadable-manifest.json +37 -30
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/948.js +2 -2
  14. package/.next/server/chunks/9563.js +1 -1
  15. package/.next/server/middleware-build-manifest.js +1 -1
  16. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  17. package/.next/server/pages/[...slug].js +1 -1
  18. package/.next/server/pages/api/graphql.js +1 -1
  19. package/.next/server/pages/api/preview.js +1 -1
  20. package/.next/server/pages/en-US/404.html +2 -2
  21. package/.next/server/pages/en-US/500.html +2 -2
  22. package/.next/server/pages/en-US/checkout.html +2 -2
  23. package/.next/server/pages/en-US/login.html +2 -2
  24. package/.next/server/pages/en-US/s.html +2 -2
  25. package/.next/server/pages/en-US.html +2 -2
  26. package/.next/server/pages-manifest.json +1 -1
  27. package/.next/static/{B1tGZHuLyVThpqeX7jh3X → -iM74ScvesldH1477MJQu}/_buildManifest.js +1 -1
  28. package/.next/static/chunks/{3166-1fd17b322ca991eb.js → 3166-50d81179a0f5a894.js} +1 -1
  29. package/.next/static/chunks/3465.af28497e8069330f.js +1 -0
  30. package/.next/static/chunks/4949.58cc42cd6109f59a.js +6 -0
  31. package/.next/static/chunks/6355.f1b1feefc0c84a2a.js +1 -0
  32. package/.next/static/chunks/{83.c68f386e6783ec51.js → 83.b87d797323ff2034.js} +1 -1
  33. package/.next/static/chunks/9173-94386b70c1626a31.js +1 -0
  34. package/.next/static/chunks/{BannerNewsletter.7f2d79c25b0e2546.js → BannerNewsletter.7c592f132e7048e5.js} +1 -1
  35. package/.next/static/chunks/{BannerText.50b22511f61fe02a.js → BannerText.695d4d4b6a3f7309.js} +1 -1
  36. package/.next/static/chunks/{CartSidebar.4822eeeb560ccb43.js → CartSidebar.a00083c44c87c268.js} +1 -1
  37. package/.next/static/chunks/PreviewTag.0b16a5b6ac35ce1c.js +1 -0
  38. package/.next/static/chunks/{ProductShelf.ffceb1cb3d66e171.js → ProductShelf.d51ba3e6a1b4a57d.js} +1 -1
  39. package/.next/static/chunks/{RegionModal.21284dd0660b4e6d.js → RegionModal.f61aa62e0a09182a.js} +1 -1
  40. package/.next/static/chunks/{Toast.4ff856492d402ad9.js → Toast.6116bc845cd67f49.js} +1 -1
  41. package/.next/static/chunks/pages/{[...slug]-34d0d47df01bbdc6.js → [...slug]-72047653203f9fd2.js} +1 -1
  42. package/.next/static/chunks/webpack-c76f1cac87402029.js +1 -0
  43. package/.next/static/css/202a74b80e6ce63f.css +1 -0
  44. package/.next/static/css/{b5bf49598c8f8b66.css → 6831395ff5fd317a.css} +1 -1
  45. package/.next/static/css/74e963fcd3434141.css +1 -0
  46. package/.next/trace +129 -128
  47. package/.turbo/turbo-build.log +14 -14
  48. package/.turbo/turbo-test.log +5 -6
  49. package/CHANGELOG.md +10 -0
  50. package/package.json +4 -4
  51. package/src/components/cms/RenderSections.tsx +18 -0
  52. package/src/components/common/PreviewTag/PreviewTag.tsx +49 -0
  53. package/src/components/common/PreviewTag/index.ts +2 -0
  54. package/src/components/common/PreviewTag/section.module.scss +16 -0
  55. package/src/pages/[...slug].tsx +1 -1
  56. package/src/pages/api/preview.ts +22 -6
  57. package/src/server/content/service.ts +100 -61
  58. package/src/server/content/types.ts +1 -1
  59. package/src/server/content/utils.ts +10 -1
  60. package/.next/static/chunks/630.400b772c0b28fb0b.js +0 -6
  61. package/.next/static/chunks/6355.3b698d9dabef2570.js +0 -1
  62. package/.next/static/chunks/6867.d82d45995c75aa12.js +0 -1
  63. package/.next/static/chunks/9173-737c162805bb7193.js +0 -1
  64. package/.next/static/chunks/webpack-cde3c2053d848754.js +0 -1
  65. package/.next/static/css/1fef663a0519dddf.css +0 -1
  66. /package/.next/static/{B1tGZHuLyVThpqeX7jh3X → -iM74ScvesldH1477MJQu}/_ssgManifest.js +0 -0
@@ -1,23 +1,23 @@
1
1
 
2
- > @faststore/core@3.60.2 prebuild /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.60.4 prebuild /home/runner/work/faststore/faststore/packages/core
3
3
  > na run partytown && na run generate
4
4
 
5
5
 
6
- > @faststore/core@3.60.2 partytown /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@3.60.4 partytown /home/runner/work/faststore/faststore/packages/core
7
7
  > partytown copylib ./public/~partytown
8
8
 
9
9
  Partytown lib copied to: /home/runner/work/faststore/faststore/packages/core/public/~partytown
10
10
 
11
- > @faststore/core@3.60.2 generate /home/runner/work/faststore/faststore/packages/core
11
+ > @faststore/core@3.60.4 generate /home/runner/work/faststore/faststore/packages/core
12
12
  > na run generate:schema && na run generate:codegen && na run format:generated
13
13
 
14
14
 
15
- > @faststore/core@3.60.2 generate:schema /home/runner/work/faststore/faststore/packages/core
15
+ > @faststore/core@3.60.4 generate:schema /home/runner/work/faststore/faststore/packages/core
16
16
  > tsx src/server/generator/generateGraphQLSchemaFile.ts
17
17
 
18
18
  Schema GraphQL file generated successfully
19
19
 
20
- > @faststore/core@3.60.2 generate:codegen /home/runner/work/faststore/faststore/packages/core
20
+ > @faststore/core@3.60.4 generate:codegen /home/runner/work/faststore/faststore/packages/core
21
21
  > graphql-codegen
22
22
 
23
23
  [STARTED] Parse Configuration
@@ -37,11 +37,11 @@ Running lifecycle hook "afterStart" scripts...
37
37
  [CLI] Loading Documents
38
38
  [CLI] Generating output
39
39
 
40
- > @faststore/core@3.60.2 format:generated /home/runner/work/faststore/faststore/packages/core
40
+ > @faststore/core@3.60.4 format:generated /home/runner/work/faststore/faststore/packages/core
41
41
  > prettier --write "@generated/**/*.{ts,js,tsx,jsx,json}" --loglevel error
42
42
 
43
43
 
44
- > @faststore/core@3.60.2 build /home/runner/work/faststore/faststore/packages/core
44
+ > @faststore/core@3.60.4 build /home/runner/work/faststore/faststore/packages/core
45
45
  > next build
46
46
 
47
47
  ⚠ No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache
@@ -62,8 +62,8 @@ Browserslist: browsers data (caniuse-lite) is 6 months old. Please run:
62
62
  Collecting page data ...
63
63
  Generating static pages (0/6) ...
64
64
 
65
65
  Generating static pages (1/6)
66
- Warning: Dynamic Content not found for the page: home. Refer to the Dynamic Content documentation at https://developers.vtex.com/docs/guides/faststore/dynamic-content-overview for mapping the page and the corresponding data-fetching function.
67
66
 
68
67
  Generating static pages (2/6)
68
+ Warning: Dynamic Content not found for the page: home. Refer to the Dynamic Content documentation at https://developers.vtex.com/docs/guides/faststore/dynamic-content-overview for mapping the page and the corresponding data-fetching function.
69
69
 
70
70
  Generating static pages (4/6)
71
71
 
72
72
  ✓ Generating static pages (6/6)
73
73
  Finalizing page optimization ...
@@ -73,22 +73,22 @@ Route (pages) Size First Load JS
73
73
  ┌ ● / 3.87 kB 133 kB
74
74
  ├ └ css/b1806cbafd0c1f81.css 3.06 kB
75
75
  ├ /_app 0 B 100 kB
76
- ├ ● /[...slug] 2.44 kB 141 kB
76
+ ├ ● /[...slug] 2.37 kB 141 kB
77
77
  ├ ● /[slug]/p 32.4 kB 162 kB
78
78
  ├ ├ css/a3ca6a9b63f657be.css 5.75 kB
79
79
  ├ ├ css/62a5153ac7061286.css 6.11 kB
80
- ├ └ css/b5bf49598c8f8b66.css 16.1 kB
80
+ ├ └ css/6831395ff5fd317a.css 16.1 kB
81
81
  ├ ○ /404 1.48 kB 131 kB
82
82
  ├ ● /500 1.48 kB 131 kB
83
83
  ├ λ /account 240 B 100 kB
84
- ├ ● /account/[...unknown] 281 B 100 kB
84
+ ├ ● /account/[...unknown] 281 B 101 kB
85
85
  ├ λ /account/403 2.33 kB 132 kB
86
86
  ├ └ css/b7bba8fce075688b.css 4.2 kB
87
- ├ λ /account/404 2.03 kB 131 kB
87
+ ├ λ /account/404 2.03 kB 132 kB
88
88
  ├ └ css/5347dbc8b71de47d.css 4.25 kB
89
89
  ├ λ /account/orders 8.57 kB 138 kB
90
90
  ├ └ css/8a3f440e0ff9cd8e.css 11.9 kB
91
- ├ λ /account/orders/[id] 10 kB 139 kB
91
+ ├ λ /account/orders/[id] 10 kB 140 kB
92
92
  ├ └ css/506442c818624bd2.css 11.5 kB
93
93
  ├ λ /account/profile 1.27 kB 131 kB
94
94
  ├ λ /account/security 1.27 kB 131 kB
@@ -104,7 +104,7 @@ Route (pages) Size First Load JS
104
104
  ├ chunks/framework-807b0f81cbc129f0.js 45.4 kB
105
105
  ├ chunks/main-f658704b53a96ab1.js 33.1 kB
106
106
  ├ chunks/pages/_app-52aa8f30f7707675.js 18.1 kB
107
- ├ chunks/webpack-cde3c2053d848754.js 3.69 kB
107
+ ├ chunks/webpack-c76f1cac87402029.js 3.73 kB
108
108
  └ css/0a57ee6c7a57788c.css 3.49 kB
109
109
 
110
110
  λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
@@ -1,15 +1,14 @@
1
1
 
2
- > @faststore/core@3.60.2 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.60.4 test /home/runner/work/faststore/faststore/packages/core
3
3
  > jest
4
4
 
5
- PASS test/server/cms/global.test.ts (25.298 s)
6
- PASS test/utils/multipleTemplates.test.ts (26.037 s)
5
+ PASS test/utils/multipleTemplates.test.ts (25.727 s)
6
+ PASS test/server/cms/global.test.ts (26.444 s)
7
7
  PASS test/server/cms/index.test.ts
8
- PASS test/server/index.test.ts (30.622 s)
9
- A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
8
+ PASS test/server/index.test.ts (31.174 s)
10
9
 
11
10
  Test Suites: 4 passed, 4 total
12
11
  Tests: 22 passed, 22 total
13
12
  Snapshots: 0 total
14
- Time: 32.048 s
13
+ Time: 32.394 s
15
14
  Ran all test suites.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.61.0](https://github.com/vtex/faststore/compare/v3.60.4...v3.61.0) (2025-07-04)
7
+
8
+ ### Features
9
+
10
+ - implement CP branch preview support ([#2885](https://github.com/vtex/faststore/issues/2885)) ([a55fe24](https://github.com/vtex/faststore/commit/a55fe24dac70ccfedc6296001bd5c18225affeda)), closes [/github.com/vtex/faststore/pull/2885/files#diff-63a7f8cba93bce2eb5ed6be3a1bcf1fbde76d643b165ae2a1a44cc8765dfb73fR40](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-63a7f8cba93bce2eb5ed6be3a1bcf1fbde76d643b165ae2a1a44cc8765dfb73fR40) [/github.com/vtex/faststore/pull/2885/files#diff-63a7f8cba93bce2eb5ed6be3a1bcf1fbde76d643b165ae2a1a44cc8765dfb73fR250](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-63a7f8cba93bce2eb5ed6be3a1bcf1fbde76d643b165ae2a1a44cc8765dfb73fR250) [/github.com/vtex/faststore/pull/2885/files#diff-8dd0ec358e808043c69af593825ff65e9afe1dd776dad04f15480a45b57a2608L36](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-8dd0ec358e808043c69af593825ff65e9afe1dd776dad04f15480a45b57a2608L36) [/github.com/vtex/faststore/pull/2885/files#diff-6728e613609d08999bccf3fc982a6a61c19d6d8eb08eb4d7adb66f4a6924843dR14](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-6728e613609d08999bccf3fc982a6a61c19d6d8eb08eb4d7adb66f4a6924843dR14) [/github.com/vtex/faststore/pull/2885/files#diff-7c7bcf90ece929b012c8fa7a7b62eac4ab72ca06aa5684add108af699bd62f50R38](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-7c7bcf90ece929b012c8fa7a7b62eac4ab72ca06aa5684add108af699bd62f50R38) [/github.com/vtex/faststore/pull/2885/files#diff-b45ed72491ecc419d59ca49fa8b4fc7a45deb55758a6a401f3c8401c11114227R148](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-b45ed72491ecc419d59ca49fa8b4fc7a45deb55758a6a401f3c8401c11114227R148) [/github.com/vtex/faststore/pull/2885/files#diff-8dd0ec358e808043c69af593825ff65e9afe1dd776dad04f15480a45b57a2608R52](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-8dd0ec358e808043c69af593825ff65e9afe1dd776dad04f15480a45b57a2608R52) [/github.com/vtex/faststore/pull/2885/files#diff-6728e613609d08999bccf3fc982a6a61c19d6d8eb08eb4d7adb66f4a6924843dR22](https://github.com//github.com/vtex/faststore/pull/2885/files/issues/diff-6728e613609d08999bccf3fc982a6a61c19d6d8eb08eb4d7adb66f4a6924843dR22) [/github.com/vtex/faststore/blob/main/packages/core/discovery.config.default.js#L128](https://github.com//github.com/vtex/faststore/blob/main/packages/core/discovery.config.default.js/issues/L128) [/github.com/vtex/faststore/blob/main/packages/core/discovery.config.default.js#L128](https://github.com//github.com/vtex/faststore/blob/main/packages/core/discovery.config.default.js/issues/L128)
11
+
12
+ ## [3.60.4](https://github.com/vtex/faststore/compare/v3.60.3...v3.60.4) (2025-07-04)
13
+
14
+ **Note:** Version bump only for package @faststore/core
15
+
6
16
  ## [3.60.3](https://github.com/vtex/faststore/compare/v3.60.2...v3.60.3) (2025-07-02)
7
17
 
8
18
  **Note:** Version bump only for package @faststore/core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "3.60.3",
3
+ "version": "3.61.0",
4
4
  "license": "MIT",
5
5
  "repository": "vtex/faststore",
6
6
  "browserslist": "supports es6-module and not dead",
@@ -48,7 +48,7 @@
48
48
  "@faststore/graphql-utils": "^3.56.1",
49
49
  "@faststore/lighthouse": "^3.56.1",
50
50
  "@faststore/sdk": "^3.60.3",
51
- "@faststore/ui": "^3.56.1",
51
+ "@faststore/ui": "^3.60.4",
52
52
  "@graphql-codegen/cli": "5.0.2",
53
53
  "@graphql-codegen/client-preset": "4.2.6",
54
54
  "@graphql-codegen/typescript": "4.0.7",
@@ -61,7 +61,7 @@
61
61
  "@parcel/watcher": "^2.4.0",
62
62
  "@types/react": "^18.2.42",
63
63
  "@vtex/client-cms": "^0.2.12",
64
- "@vtex/client-cp": "0.2.2",
64
+ "@vtex/client-cp": "0.3.0",
65
65
  "@vtex/prettier-config": "1.0.0",
66
66
  "autoprefixer": "^10.4.0",
67
67
  "css-loader": "^6.7.1",
@@ -105,5 +105,5 @@
105
105
  "ts-jest": "29.1.1",
106
106
  "typescript": "5.3.2"
107
107
  },
108
- "gitHead": "e0f5b29a904c59d59bdf348d5d6caebd311358fe"
108
+ "gitHead": "2ba46c81202a84ae4a13e61e7a453c034df6b187"
109
109
  }
@@ -9,7 +9,9 @@ import {
9
9
  import { useUI } from '@faststore/ui'
10
10
  import type { Section } from '@vtex/client-cms'
11
11
  import dynamic from 'next/dynamic'
12
+ import { useRouter } from 'next/router'
12
13
  import useTTI from 'src/sdk/performance/useTTI'
14
+ import { isContentPlatformSource } from 'src/server/content/utils'
13
15
  import SectionBoundary from './SectionBoundary'
14
16
  import ViewportObserver from './ViewportObserver'
15
17
  import COMPONENTS from './global/Components'
@@ -28,6 +30,11 @@ const Toast = dynamic(
28
30
  { ssr: false }
29
31
  )
30
32
 
33
+ const PreviewTag = dynamic(
34
+ () => import(/* webpackChunkName: "PreviewTag" */ '../common/PreviewTag'),
35
+ { ssr: false }
36
+ )
37
+
31
38
  const useDividedSections = (sections: Section[]) => {
32
39
  return useMemo(() => {
33
40
  const indexChildren = sections.findIndex(({ name }) => name === 'Children')
@@ -137,9 +144,20 @@ function RenderSections({
137
144
  )
138
145
 
139
146
  const { isInteractive } = useTTI()
147
+ const router = useRouter()
148
+
149
+ const shouldDisplayPreviewTag = isContentPlatformSource() && router.isPreview
140
150
 
141
151
  return (
142
152
  <>
153
+ {shouldDisplayPreviewTag && (
154
+ <LazyLoadingSection
155
+ sectionName="PreviewTag"
156
+ isInteractive={isInteractive}
157
+ >
158
+ <PreviewTag />
159
+ </LazyLoadingSection>
160
+ )}
143
161
  {firstSections && (
144
162
  <RenderSectionsBase
145
163
  sections={firstSections}
@@ -0,0 +1,49 @@
1
+ import { useEffect } from 'react'
2
+ import { Tag as UITag } from '@faststore/ui'
3
+ import Section from 'src/components/sections/Section/Section'
4
+
5
+ import styles from './section.module.scss'
6
+
7
+ export interface PreviewTagProps {
8
+ text?: string
9
+ exitUrl?: string
10
+ }
11
+
12
+ function PreviewTag({ text = 'Preview', exitUrl }: PreviewTagProps) {
13
+ useEffect(() => {
14
+ const handleBeforeUnload = () => {
15
+ navigator.sendBeacon('/api/preview?action=clear')
16
+ }
17
+
18
+ window.addEventListener('beforeunload', handleBeforeUnload)
19
+
20
+ return () => {
21
+ window.removeEventListener('beforeunload', handleBeforeUnload)
22
+ }
23
+ }, [])
24
+
25
+ const handleExitPreview = () => {
26
+ if (typeof window !== 'undefined') {
27
+ const exitPath =
28
+ exitUrl || window.location.pathname + window.location.search
29
+ window.location.href = `/api/preview?action=clear&redirect=${encodeURIComponent(
30
+ exitPath
31
+ )}`
32
+ }
33
+ }
34
+
35
+ return (
36
+ <Section className={`${styles.section} section-preview-tag`}>
37
+ <UITag
38
+ data-fs-preview-tag
39
+ testId="fs-preview-tag"
40
+ variant="danger"
41
+ label={text}
42
+ iconButtonLabel="Exit preview"
43
+ onClose={handleExitPreview}
44
+ />
45
+ </Section>
46
+ )
47
+ }
48
+
49
+ export default PreviewTag
@@ -0,0 +1,2 @@
1
+ export { default } from './PreviewTag'
2
+ export type { PreviewTagProps } from './PreviewTag'
@@ -0,0 +1,16 @@
1
+ @layer components {
2
+ .section {
3
+ @import "@faststore/ui/src/components/atoms/Badge/styles.scss";
4
+ @import "@faststore/ui/src/components/atoms/Icon/styles.scss";
5
+ @import "@faststore/ui/src/components/molecules/Tag/styles.scss";
6
+
7
+ [data-fs-preview-tag] {
8
+ box-shadow: var(--fs-shadow-darker);
9
+ backdrop-filter: blur(var(--fs-spacing-1));
10
+ position: fixed;
11
+ top: 1rem;
12
+ right: 1rem;
13
+ z-index: 10000;
14
+ }
15
+ }
16
+ }
@@ -119,7 +119,7 @@ export const getStaticProps: GetStaticProps<
119
119
 
120
120
  const landingPage = await landingPagePromise
121
121
 
122
- if (landingPage) {
122
+ if (landingPage && Object.keys(landingPage).length > 0) {
123
123
  const [
124
124
  serverData,
125
125
  globalSections,
@@ -1,9 +1,13 @@
1
- import type { NextApiHandler, NextApiRequest } from 'next'
1
+ import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
2
2
 
3
3
  import { previewRedirects } from '../../../discovery.config'
4
4
  import { contentService } from 'src/server/content/service'
5
5
  import { isLocator } from 'src/server/cms'
6
- import { isContentPlatformSource } from 'src/server/content/utils'
6
+ import {
7
+ isBranchPreview,
8
+ isContentPlatformSource,
9
+ } from 'src/server/content/utils'
10
+ import type { PreviewData } from 'src/server/content/types'
7
11
 
8
12
  type Settings = {
9
13
  seo: {
@@ -27,20 +31,32 @@ const pickParam = (req: NextApiRequest, parameter: string) => {
27
31
  }
28
32
 
29
33
  const setPreviewAndRedirect = (
30
- res: any,
34
+ res: NextApiResponse,
31
35
  previewData: Record<string, string>,
32
36
  redirectPath: string
33
37
  ) => {
34
- res.setPreviewData(previewData, {
38
+ const options: { maxAge: number; path?: string } = {
35
39
  maxAge: 3600,
36
- path: redirectPath,
37
- })
40
+ }
41
+
42
+ if (!isBranchPreview(previewData as PreviewData)) {
43
+ options.path = redirectPath
44
+ }
45
+
46
+ res.setPreviewData(previewData, options)
38
47
  res.redirect(redirectPath)
39
48
  }
40
49
 
41
50
  // TODO: Improve security by disabling CMS preview in production
42
51
  const handler: NextApiHandler = async (req, res) => {
43
52
  try {
53
+ if (pickParam(req, 'action') === 'clear') {
54
+ res.clearPreviewData()
55
+ const redirectTo = pickParam(req, 'redirect') || '/'
56
+ res.redirect(redirectTo)
57
+ return
58
+ }
59
+
44
60
  let slug = pickParam(req, 'slug')
45
61
  if (slug && !slug.startsWith('/')) {
46
62
  slug = `/${slug}`
@@ -2,7 +2,7 @@ import type { ContentData, Locator } from '@vtex/client-cms'
2
2
  import ClientCP from '@vtex/client-cp'
3
3
  import type { ContentEntry, EntryPathParams } from '@vtex/client-cp'
4
4
  import { getCMSPage, getPage, type PageContentType } from 'src/server/cms'
5
- import type { ContentOptions, ContentParams } from './types'
5
+ import type { ContentOptions, ContentParams, PreviewData } from './types'
6
6
  import config from '../../../discovery.config'
7
7
  import { getPLP, type PLPContentType } from '../cms/plp'
8
8
  import {
@@ -15,7 +15,9 @@ import MissingContentError from 'src/sdk/error/MissingContentError'
15
15
  import { getPDP, type PDPContentType } from '../cms/pdp'
16
16
  import MultipleContentError from 'src/sdk/error/MultipleContentError'
17
17
  import type { ServerProductQueryQuery } from '@generated/graphql'
18
- import { isContentPlatformSource } from './utils'
18
+ import { isBranchPreview, isContentPlatformSource } from './utils'
19
+
20
+ type ContentResult = ContentData | (ContentEntry & PageContentType)
19
21
 
20
22
  export class ContentService {
21
23
  private clientCP: ClientCP
@@ -37,22 +39,15 @@ export class ContentService {
37
39
  return getPage(options.cmsOptions)
38
40
  }
39
41
 
40
- async getContent(params: ContentParams) {
42
+ async getMultipleContent(
43
+ params: ContentParams
44
+ ): Promise<{ data: ContentResult[] }> {
41
45
  const options = this.createContentOptions(params)
42
46
 
43
47
  if (isContentPlatformSource()) {
44
48
  const serviceParams = this.convertOptionsToParams(options)
45
49
  const { entries } = await this.clientCP.listEntries(serviceParams)
46
- const data = await Promise.all(
47
- entries.map(async (entry) => {
48
- const entryData = await this.getSingleEntry(
49
- { ...serviceParams, entryId: entry.id },
50
- !!options.isPreview
51
- )
52
- return this.mergeEntryWithData(entry, entryData)
53
- })
54
- )
55
- return { data }
50
+ return this.fillEntriesWithData(entries, serviceParams, options.isPreview)
56
51
  }
57
52
  return getCMSPage(options.cmsOptions)
58
53
  }
@@ -65,10 +60,10 @@ export class ContentService {
65
60
  const options = this.createContentOptions(plpParams)
66
61
 
67
62
  if (isContentPlatformSource()) {
68
- const pages = (await this.getContent(plpParams)).data
63
+ const pages = (await this.getMultipleContent(plpParams)).data
69
64
  if (!pages?.length) throw new MissingContentError(options.cmsOptions)
70
65
  return findBestPLPTemplate(
71
- pages,
66
+ pages as Partial<PLPContentType>[],
72
67
  options.slug,
73
68
  rewrites
74
69
  ) as PLPContentType
@@ -84,9 +79,12 @@ export class ContentService {
84
79
  const options = this.createContentOptions(pdpParams)
85
80
 
86
81
  if (isContentPlatformSource()) {
87
- const pages = (await this.getContent(pdpParams)).data
82
+ const pages = (await this.getMultipleContent(pdpParams)).data
88
83
  if (!pages.length) throw new MissingContentError(options.cmsOptions)
89
- return findBestPDPTemplate(pages, product) as PDPContentType
84
+ return findBestPDPTemplate(
85
+ pages as Partial<PDPContentType>[],
86
+ product
87
+ ) as PDPContentType
90
88
  }
91
89
  return getPDP(product, options.cmsOptions as Locator)
92
90
  }
@@ -96,64 +94,92 @@ export class ContentService {
96
94
  ): Promise<T> {
97
95
  const params = this.convertOptionsToParams(options)
98
96
  try {
99
- const entry: PageContentType =
100
- params.entryId || params.slug
101
- ? await this.getSingleEntry(params, !!options.isPreview)
102
- : await this.fetchFirstEntry(params, !!options.isPreview)
103
-
97
+ const entry: PageContentType = await this.getEntry(
98
+ params,
99
+ options.isPreview
100
+ )
104
101
  return entry as T
105
102
  } catch (err: unknown) {
106
- if (isNotFoundError(err)) console.error('Content not found', err)
103
+ if (isNotFoundError(err)) console.warn('Content not found', err)
107
104
  else throw err
108
105
  }
109
106
  }
110
107
 
111
- private async getSingleEntry(
108
+ private async fillEntriesWithData(
109
+ entries: ContentEntry[],
110
+ serviceParams: EntryPathParams,
111
+ isPreview: boolean
112
+ ): Promise<{ data: (ContentEntry & PageContentType)[] }> {
113
+ const data = await Promise.all(
114
+ entries.map(async (entry) => {
115
+ const entryData = await this.getEntryData(
116
+ { ...serviceParams, entryId: entry.id },
117
+ isPreview
118
+ )
119
+ return this.mergeEntryWithData(entry, entryData)
120
+ })
121
+ )
122
+ return { data }
123
+ }
124
+
125
+ private async getEntry(
126
+ params: EntryPathParams,
127
+ isPreview: boolean
128
+ ): Promise<PageContentType> {
129
+ return params.entryId || params.slug
130
+ ? await this.getEntryData(params, isPreview)
131
+ : await this.fetchFirstEntryFromList(params, isPreview)
132
+ }
133
+
134
+ private async getEntryData(
112
135
  params: EntryPathParams,
113
136
  isPreview: boolean
114
137
  ): Promise<PageContentType> {
138
+ if (!params.entryId && !params.slug) {
139
+ const operation = isPreview ? 'Preview' : 'getEntry'
140
+ throw new Error(`${operation} requires entryId or slug`)
141
+ }
142
+
115
143
  if (isPreview) {
116
- if (params.entryId)
117
- return this.clientCP.previewEntryById(
118
- params
119
- ) as Promise<PageContentType>
120
- if (params.slug)
121
- return this.clientCP.previewEntryBySlug(
122
- params
123
- ) as Promise<PageContentType>
124
- throw new Error('Preview requires entryId or slug')
125
- }
126
- if (params.entryId)
127
- return this.clientCP.getEntry(params) as Promise<PageContentType>
128
- if (params.slug)
129
- return this.clientCP.getEntryBySlug(params) as Promise<PageContentType>
130
- throw new Error('getEntry requires entryId or slug')
144
+ return params.entryId
145
+ ? (this.clientCP.previewEntryById(params) as Promise<PageContentType>)
146
+ : (this.clientCP.previewEntryBySlug(params) as Promise<PageContentType>)
147
+ }
148
+
149
+ return params.entryId
150
+ ? (this.clientCP.getEntry(params) as Promise<PageContentType>)
151
+ : (this.clientCP.getEntryBySlug(params) as Promise<PageContentType>)
131
152
  }
132
153
 
133
- private async fetchFirstEntry(
154
+ private async fetchFirstEntryFromList(
134
155
  params: EntryPathParams,
135
156
  isPreview: boolean
136
157
  ): Promise<PageContentType> {
137
158
  const { entries } = await this.clientCP.listEntries(params)
138
159
  if (!entries || entries.length === 0) {
139
- console.error('No entries found for params', params)
160
+ console.warn('No entries found for params', params)
140
161
  return {} as PageContentType
141
162
  }
142
163
  if (entries.length > 1) {
143
164
  throw new MultipleContentError(params)
144
165
  }
145
- return this.getSingleEntry({ ...params, entryId: entries[0].id }, isPreview)
166
+ return this.getEntryData({ ...params, entryId: entries[0].id }, isPreview)
146
167
  }
147
168
 
148
- private mergeEntryWithData(
149
- entry: ContentEntry,
150
- data: PageContentType
151
- ): ContentEntry & PageContentType {
169
+ private createContentOptions(params: ContentParams): ContentOptions {
170
+ const { contentType, previewData, slug } = params
171
+
172
+ const contentPreviewEnabled = previewData?.contentType === contentType
173
+ const branchPreviewEnabled = isBranchPreview(previewData)
174
+
152
175
  return {
153
- ...entry,
154
- ...data,
155
- id: entry.id,
156
- name: entry.name || '',
176
+ cmsOptions: this.buildCmsOptions(
177
+ params,
178
+ contentPreviewEnabled,
179
+ branchPreviewEnabled
180
+ ),
181
+ ...(slug !== undefined && { slug }),
182
+ isPreview: contentPreviewEnabled || branchPreviewEnabled,
157
183
  }
158
184
  }
159
185
 
@@ -176,8 +202,10 @@ export class ContentService {
176
202
  params.branchId = cmsOptions.releaseId
177
203
  }
178
204
  if ('filters' in cmsOptions && cmsOptions.filters) {
179
- const nested = cmsOptions.filters.filters as Record<string, any>
180
- if (nested['settings.seo.slug']) {
205
+ const nested = (
206
+ cmsOptions.filters as { filters?: Record<string, unknown> }
207
+ ).filters as Record<string, unknown>
208
+ if (nested && nested['settings.seo.slug']) {
181
209
  const seo = nested['settings.seo.slug'] as string
182
210
  params.slug = seo.replace(/^\//, '')
183
211
  }
@@ -187,33 +215,44 @@ export class ContentService {
187
215
  return params as EntryPathParams
188
216
  }
189
217
 
190
- private createContentOptions(params: ContentParams): ContentOptions {
218
+ private buildCmsOptions(
219
+ params: ContentParams,
220
+ isContentPreview: boolean,
221
+ isBranchPreview: boolean
222
+ ) {
191
223
  const {
192
224
  contentType,
193
225
  previewData,
194
- slug,
195
226
  documentId,
196
227
  versionId,
197
228
  releaseId,
198
229
  filters,
199
230
  } = params
200
-
201
- const isPreview = previewData?.contentType === contentType
202
231
  const { slug: _, ...previewLocator } = previewData || {}
203
232
 
204
- const cmsOptions = {
233
+ return {
205
234
  contentType,
206
- ...(isPreview ? previewLocator : {}),
235
+ ...(isContentPreview ? previewLocator : {}),
236
+ ...(isBranchPreview && {
237
+ versionId: previewData?.versionId,
238
+ releaseId: previewData?.releaseId,
239
+ }),
207
240
  ...(documentId !== undefined && { documentId }),
208
241
  ...(versionId !== undefined && { versionId }),
209
242
  ...(releaseId !== undefined && { releaseId }),
210
243
  ...(filters && { filters }),
211
244
  }
245
+ }
212
246
 
247
+ private mergeEntryWithData(
248
+ entry: ContentEntry,
249
+ data: PageContentType
250
+ ): ContentEntry & PageContentType {
213
251
  return {
214
- cmsOptions,
215
- ...(slug !== undefined && { slug }),
216
- isPreview,
252
+ ...entry,
253
+ ...data,
254
+ id: entry.id,
255
+ name: entry.name || '',
217
256
  }
218
257
  }
219
258
  }
@@ -16,7 +16,7 @@ export interface ContentParams {
16
16
  documentId?: string
17
17
  versionId?: string
18
18
  releaseId?: string
19
- filters?: Record<string, any>
19
+ filters?: Record<string, unknown>
20
20
  }
21
21
 
22
22
  export interface ContentOptions {
@@ -1,8 +1,17 @@
1
1
  import { contentSource } from '../../../discovery.config'
2
- import { ContentSourceType } from './types'
2
+ import { ContentSourceType, type PreviewData } from './types'
3
3
 
4
4
  export function isContentPlatformSource(): boolean {
5
5
  return (
6
6
  contentSource.type.toLocaleLowerCase() === ContentSourceType.ContentPlatform
7
7
  )
8
8
  }
9
+
10
+ export function isBranchPreview(
11
+ previewData: PreviewData | null | undefined
12
+ ): boolean {
13
+ return (
14
+ isContentPlatformSource() &&
15
+ !!(previewData?.versionId || previewData?.releaseId)
16
+ )
17
+ }