@prismetic/next-preview-core 1.0.0 → 1.0.1

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.
@@ -0,0 +1,41 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React$1 from 'react';
3
+
4
+ /**
5
+ * React Query provider for Preview Mode.
6
+ * Part of @prismetic/next-preview-core.
7
+ *
8
+ * Wrap the preview build's component tree in this provider to enable
9
+ * client-side caching of Strapi data. Layout data (Navbar, Footer)
10
+ * fetched once will be served from the cache on subsequent page navigations.
11
+ *
12
+ * @param {number} [staleTime=300000] - How long (ms) cached data is considered fresh. Default: 5 minutes.
13
+ */
14
+ declare function PreviewProviders({ children, staleTime }: {
15
+ children: React.ReactNode;
16
+ staleTime?: number;
17
+ }): react_jsx_runtime.JSX.Element;
18
+
19
+ /**
20
+ * A generic Higher-Order Component for Preview Mode data fetching.
21
+ * Part of @prismetic/next-preview-core.
22
+ *
23
+ * @param {React.Component} WrappedComponent - The pure UI component to render once data is fetched.
24
+ * @param {Function} fetcherFn - An async function that fetches data (receives component props as argument).
25
+ * @param {Function} cacheKeyGenerator - A function that returns a React Query queryKey array (receives component props).
26
+ *
27
+ * @example
28
+ * // In your project's preview wrapper:
29
+ * import { withLivePreview } from "@prismetic/next-preview-core";
30
+ * import HomeUI from "../HomeUI";
31
+ * import { fetchHomepageContent } from "@/api/queryModules";
32
+ *
33
+ * export const PreviewHome = withLivePreview(
34
+ * HomeUI,
35
+ * ({ locale }) => fetchHomepageContent(locale),
36
+ * ({ locale }) => ["homepage", locale]
37
+ * );
38
+ */
39
+ declare function withLivePreview<P extends object>(WrappedComponent: React$1.ComponentType<P>, fetcherFn: (props: P) => Promise<any>, cacheKeyGenerator: (props: P) => any[]): (props: P) => react_jsx_runtime.JSX.Element;
40
+
41
+ export { PreviewProviders, withLivePreview };
@@ -0,0 +1,41 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import React$1 from 'react';
3
+
4
+ /**
5
+ * React Query provider for Preview Mode.
6
+ * Part of @prismetic/next-preview-core.
7
+ *
8
+ * Wrap the preview build's component tree in this provider to enable
9
+ * client-side caching of Strapi data. Layout data (Navbar, Footer)
10
+ * fetched once will be served from the cache on subsequent page navigations.
11
+ *
12
+ * @param {number} [staleTime=300000] - How long (ms) cached data is considered fresh. Default: 5 minutes.
13
+ */
14
+ declare function PreviewProviders({ children, staleTime }: {
15
+ children: React.ReactNode;
16
+ staleTime?: number;
17
+ }): react_jsx_runtime.JSX.Element;
18
+
19
+ /**
20
+ * A generic Higher-Order Component for Preview Mode data fetching.
21
+ * Part of @prismetic/next-preview-core.
22
+ *
23
+ * @param {React.Component} WrappedComponent - The pure UI component to render once data is fetched.
24
+ * @param {Function} fetcherFn - An async function that fetches data (receives component props as argument).
25
+ * @param {Function} cacheKeyGenerator - A function that returns a React Query queryKey array (receives component props).
26
+ *
27
+ * @example
28
+ * // In your project's preview wrapper:
29
+ * import { withLivePreview } from "@prismetic/next-preview-core";
30
+ * import HomeUI from "../HomeUI";
31
+ * import { fetchHomepageContent } from "@/api/queryModules";
32
+ *
33
+ * export const PreviewHome = withLivePreview(
34
+ * HomeUI,
35
+ * ({ locale }) => fetchHomepageContent(locale),
36
+ * ({ locale }) => ["homepage", locale]
37
+ * );
38
+ */
39
+ declare function withLivePreview<P extends object>(WrappedComponent: React$1.ComponentType<P>, fetcherFn: (props: P) => Promise<any>, cacheKeyGenerator: (props: P) => any[]): (props: P) => react_jsx_runtime.JSX.Element;
40
+
41
+ export { PreviewProviders, withLivePreview };
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ PreviewProviders: () => PreviewProviders,
25
+ withLivePreview: () => withLivePreview
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/PreviewProviders.tsx
30
+ var import_react_query = require("@tanstack/react-query");
31
+ var import_react = require("react");
32
+ var import_jsx_runtime = require("react/jsx-runtime");
33
+ function PreviewProviders({ children, staleTime = 5 * 60 * 1e3 }) {
34
+ const [queryClient] = (0, import_react.useState)(
35
+ () => new import_react_query.QueryClient({
36
+ defaultOptions: {
37
+ queries: {
38
+ staleTime,
39
+ refetchOnWindowFocus: false,
40
+ retry: 1
41
+ }
42
+ }
43
+ })
44
+ );
45
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_query.QueryClientProvider, { client: queryClient, children });
46
+ }
47
+
48
+ // src/withLivePreview.tsx
49
+ var import_react_query2 = require("@tanstack/react-query");
50
+ var import_jsx_runtime2 = require("react/jsx-runtime");
51
+ function withLivePreview(WrappedComponent, fetcherFn, cacheKeyGenerator) {
52
+ return function PreviewComponent(props) {
53
+ const queryKey = cacheKeyGenerator(props);
54
+ const { data, isLoading } = (0, import_react_query2.useQuery)({
55
+ queryKey,
56
+ queryFn: () => fetcherFn(props)
57
+ });
58
+ if (isLoading) {
59
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }, children: "Loading preview..." });
60
+ }
61
+ if (!data) {
62
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: "Page not found" });
63
+ }
64
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WrappedComponent, { ...props, ...data });
65
+ };
66
+ }
67
+ // Annotate the CommonJS export names for ESM import in node:
68
+ 0 && (module.exports = {
69
+ PreviewProviders,
70
+ withLivePreview
71
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ // src/PreviewProviders.tsx
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { useState } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ function PreviewProviders({ children, staleTime = 5 * 60 * 1e3 }) {
8
+ const [queryClient] = useState(
9
+ () => new QueryClient({
10
+ defaultOptions: {
11
+ queries: {
12
+ staleTime,
13
+ refetchOnWindowFocus: false,
14
+ retry: 1
15
+ }
16
+ }
17
+ })
18
+ );
19
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children });
20
+ }
21
+
22
+ // src/withLivePreview.tsx
23
+ import { useQuery } from "@tanstack/react-query";
24
+ import { jsx as jsx2 } from "react/jsx-runtime";
25
+ function withLivePreview(WrappedComponent, fetcherFn, cacheKeyGenerator) {
26
+ return function PreviewComponent(props) {
27
+ const queryKey = cacheKeyGenerator(props);
28
+ const { data, isLoading } = useQuery({
29
+ queryKey,
30
+ queryFn: () => fetcherFn(props)
31
+ });
32
+ if (isLoading) {
33
+ return /* @__PURE__ */ jsx2("div", { style: { minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }, children: "Loading preview..." });
34
+ }
35
+ if (!data) {
36
+ return /* @__PURE__ */ jsx2("div", { children: "Page not found" });
37
+ }
38
+ return /* @__PURE__ */ jsx2(WrappedComponent, { ...props, ...data });
39
+ };
40
+ }
41
+ export {
42
+ PreviewProviders,
43
+ withLivePreview
44
+ };
@@ -0,0 +1,151 @@
1
+ # Implementing Hybrid Static/Preview Architecture
2
+
3
+ This guide explains how to implement the `@prismetic/next-preview-core` architecture into any new or existing Next.js + Strapi project.
4
+
5
+ ## 🔴 The Problem
6
+ Modern Next.js applications often use `output: "export"` to build pure static HTML pages for maximum performance and security. However, this creates a major pain point for content editors: **Previewing content.**
7
+ - During a static build, the server fetches data from Strapi and bakes it into HTML.
8
+ - If an editor changes content in Strapi, they cannot see their changes until the entire site is rebuilt via CI/CD, which takes minutes.
9
+ - To provide a live preview, a persistent Node.js server is typically required, defeating the cost/security benefits of a purely static S3/CDN deployment.
10
+
11
+ ## 🟢 The Solution
12
+ A **Hybrid Architecture**. We maintain two separate build outputs using an environment variable (`NEXT_PUBLIC_APP_ENV`):
13
+ 1. **Production Build (`production`)**: Normal static export. Data is fetched at build time. Deployed to the production S3 bucket.
14
+ 2. **Preview Build (`preview`)**: A unique static export. Instead of fetching data at build time, it builds static "shell" components. When the editor loads the page in the browser, these shells use React Query (`@tanstack/react-query`) to fetch the latest draft content *directly from the Strapi GraphQL API* on the client-side.
15
+
16
+ This provides the best of both worlds: highly secure static production sites, and instant live previews hosted on S3 that require zero backend compute.
17
+
18
+ ---
19
+
20
+ ## 🚀 Step-by-Step Implementation Guide
21
+
22
+ Follow these steps when setting up a new Next.js project.
23
+
24
+ ### Step 1: Install Dependencies
25
+
26
+ ```bash
27
+ npm install @prismetic/next-preview-core @tanstack/react-query
28
+ ```
29
+
30
+ ### Step 2: Update Build Scripts
31
+ In your [package.json](file:///Users/sunnyjhunjhunwala/PSM/repos/psm/next-preview-core/package.json), add a script for the preview build:
32
+
33
+ ```json
34
+ "scripts": {
35
+ "build": "next build",
36
+ "build:preview": "NEXT_PUBLIC_APP_ENV=preview next build"
37
+ }
38
+ ```
39
+
40
+ ### Step 3: Add the Provider to the Root Layout
41
+ Wrap your preview environment in [PreviewProviders](file:///Users/sunnyjhunjhunwala/PSM/repos/psm/next-preview-core/src/PreviewProviders.tsx#6-36) to enable React Query caching. This ensures global data (like Navbar/Footer) is fetched once and cached during client-side navigation.
42
+
43
+ **[src/app/[locale]/layout.jsx](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/app/%5Blocale%5D/layout.jsx)**
44
+ ```jsx
45
+ import { PreviewProviders } from "@prismetic/next-preview-core";
46
+ // ... import your static Header/Footer and dynamic PreviewLayoutFetcher
47
+
48
+ export default async function LocaleLayout({ children, params }) {
49
+ const { locale } = await params;
50
+ const isPreview = process.env.NEXT_PUBLIC_APP_ENV === "preview";
51
+
52
+ if (isPreview) {
53
+ return (
54
+ <PreviewProviders>
55
+ <PreviewLayoutFetcher locale={locale}>
56
+ {children}
57
+ </PreviewLayoutFetcher>
58
+ </PreviewProviders>
59
+ );
60
+ }
61
+
62
+ // Production: fetch statically
63
+ const layoutData = await fetchLayoutData(locale);
64
+ return (
65
+ <>
66
+ <Header data={layoutData.header} />
67
+ <main>{children}</main>
68
+ <Footer data={layoutData.footer} />
69
+ </>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ### Step 4: Separate Pure UI from Page Logic
75
+ Never put data fetching and UI rendering in the same component. Extract the presentation layer into its own file (e.g., [HomeUI.jsx](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/app/%5Blocale%5D/HomeUI.jsx), [DynamicPageUI.jsx](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/app/%5Blocale%5D/%5B...slug%5D/DynamicPageUI.jsx)).
76
+
77
+ **[src/app/[locale]/HomeUI.jsx](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/app/%5Blocale%5D/HomeUI.jsx)**
78
+ ```jsx
79
+ // A pure function that just takes data and renders standard components
80
+ export default function HomeUI({ homepageData }) {
81
+ return (
82
+ <div>
83
+ <Hero data={homepageData.hero} />
84
+ <BlockManager blocks={homepageData.blocks} />
85
+ </div>
86
+ );
87
+ }
88
+ ```
89
+
90
+ ### Step 5: Create the Preview Wrapper
91
+ Use the [withLivePreview](file:///Users/sunnyjhunjhunwala/PSM/repos/psm/next-preview-core/src/withLivePreview.tsx#5-50) HOC to wrap the pure UI component. Give it your data fetching function and a unique cache key.
92
+
93
+ **[src/components/preview/PreviewHome.jsx](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/components/preview/PreviewHome.jsx)**
94
+ ```jsx
95
+ "use client";
96
+
97
+ import { withLivePreview } from "@prismetic/next-preview-core";
98
+ import HomeUI from "@/app/[locale]/HomeUI";
99
+ import { fetchHomepageContent } from "@/api/queryModules";
100
+
101
+ export const PreviewHome = withLivePreview(
102
+ HomeUI, // The pure UI
103
+ ({ locale }) => fetchHomepageContent(locale), // The data fetcher
104
+ ({ locale }) => ["homepage", locale] // The React Query cache key
105
+ );
106
+ ```
107
+
108
+ ### Step 6: Update the Page Route
109
+ Conditionally render the Preview Wrapper or the Static UI based on the environment variable.
110
+
111
+ **[src/app/[locale]/page.jsx](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/app/%5Blocale%5D/page.jsx)**
112
+ ```jsx
113
+ import { PreviewHome } from "@/components/preview/PreviewHome";
114
+ import HomeUI from "./HomeUI";
115
+ import { fetchHomepageContent } from "@/api/queryModules";
116
+
117
+ export default async function LocaleHomePage({ params }) {
118
+ const { locale } = await params;
119
+ const isPreview = process.env.NEXT_PUBLIC_APP_ENV === "preview";
120
+
121
+ // If preview mode, return the client-side fetching shell
122
+ if (isPreview) return <PreviewHome locale={locale} />;
123
+
124
+ // Production mode: fetch statically during build
125
+ const data = await fetchHomepageContent(locale);
126
+ return <HomeUI {...data} />; // Make sure to match prop names
127
+ }
128
+ ```
129
+
130
+ ### Step 7: Strapi CORS Configuration
131
+ For preview mode to work, the browser must be able to make a request directly to Strapi. Ensure your Strapi server's `config/middlewares.js` has appropriate CORS rules allowing the preview frontend's domain.
132
+
133
+ ```javascript
134
+ // Example Strapi CORS config
135
+ {
136
+ name: 'strapi::cors',
137
+ config: {
138
+ origin: ['https://preview.yourproject.com', 'http://localhost:3000'],
139
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
140
+ headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'],
141
+ keepHeaderOnError: true,
142
+ },
143
+ }
144
+ ```
145
+
146
+ ---
147
+
148
+ ## ✅ Final Validation
149
+ 1. Run `npm run build:preview`.
150
+ 2. Inspect [out/index.html](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/out/index.html) — it should be a nearly empty shell importing JavaScript bundles, not fully populated content.
151
+ 3. Serve [out/](file:///Users/sunnyjhunjhunwala/PSM/repos/RedSeaHospitality/turtle-bay-microsite/src/app/%5Blocale%5D/layout.jsx#16-50) locally and view the Network tab in your browser — you should see GraphQL requests fetching data natively on page load.
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@prismetic/next-preview-core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Hybrid static/preview architecture for Next.js + Strapi projects",
5
- "main": "./src/index.ts",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
6
8
  "exports": {
7
- ".": "./src/index.ts"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
8
14
  },
9
15
  "keywords": [
10
16
  "next.js",
@@ -12,9 +18,19 @@
12
18
  "preview",
13
19
  "static"
14
20
  ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch"
24
+ },
15
25
  "license": "MIT",
16
26
  "peerDependencies": {
17
- "react": ">=18",
18
- "@tanstack/react-query": ">=5"
27
+ "@tanstack/react-query": ">=5",
28
+ "react": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.3.0",
32
+ "@types/react": "^19.2.14",
33
+ "tsup": "^8.5.1",
34
+ "typescript": "^5.9.3"
19
35
  }
20
- }
36
+ }
@@ -13,7 +13,7 @@ import { useState } from "react";
13
13
  *
14
14
  * @param {number} [staleTime=300000] - How long (ms) cached data is considered fresh. Default: 5 minutes.
15
15
  */
16
- export function PreviewProviders({ children, staleTime = 5 * 60 * 1000 }) {
16
+ export function PreviewProviders({ children, staleTime = 5 * 60 * 1000 }: { children: React.ReactNode; staleTime?: number }) {
17
17
  const [queryClient] = useState(
18
18
  () =>
19
19
  new QueryClient({
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useQuery } from "@tanstack/react-query";
4
+ import React from "react";
4
5
 
5
6
  /**
6
7
  * A generic Higher-Order Component for Preview Mode data fetching.
@@ -22,8 +23,12 @@ import { useQuery } from "@tanstack/react-query";
22
23
  * ({ locale }) => ["homepage", locale]
23
24
  * );
24
25
  */
25
- export function withLivePreview(WrappedComponent, fetcherFn, cacheKeyGenerator) {
26
- return function PreviewComponent(props) {
26
+ export function withLivePreview<P extends object>(
27
+ WrappedComponent: React.ComponentType<P>,
28
+ fetcherFn: (props: P) => Promise<any>,
29
+ cacheKeyGenerator: (props: P) => any[]
30
+ ) {
31
+ return function PreviewComponent(props: P) {
27
32
  const queryKey = cacheKeyGenerator(props);
28
33
 
29
34
  const { data, isLoading } = useQuery({
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "esnext",
5
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler",
8
+ "strict": true,
9
+ "jsx": "react-jsx",
10
+ "declaration": true,
11
+ "baseUrl": "."
12
+ },
13
+ "include": ["src/**/*"]
14
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ clean: true,
8
+ external: ["react", "react-dom", "@tanstack/react-query"],
9
+ esbuildOptions(options) {
10
+ // Force Next.js App Router client directive on the output bundles
11
+ options.banner = {
12
+ js: '"use client";',
13
+ };
14
+ },
15
+ });