@blocklify/preview 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @blocklify/preview
2
2
 
3
- Frontend SDK for [Blocklify](https://github.com/): Content API client + live-preview React helpers for your theme.
3
+ Frontend SDK for [Blocklify](https://www.blocklify.app): Content API client, live preview, and starter blocks for a new website.
4
4
 
5
5
  ## Install
6
6
 
@@ -9,13 +9,66 @@ bun add @blocklify/preview
9
9
  # or: npm install @blocklify/preview
10
10
  ```
11
11
 
12
- Peer dependencies: `react`, `react-dom` (≥18).
12
+ Peer dependencies: `react` and `react-dom` (≥18).
13
13
 
14
- ## Live preview (theme blocks)
14
+ ## New Next.js site
15
15
 
16
- 1. Mount `BlocklifyPreview` on a `/preview` route with your `blockMap`.
17
- 2. In each theme block, accept `BlocklifyBlockProps` and spread `{...BlocklifyEditable}` on the root element.
18
- 3. Read CMS custom fields from `block` (e.g. `block.headline`).
19
- 4. Register that preview URL under Space → Live preview URLs.
16
+ ```bash
17
+ bunx create-next-app@latest my-blocklify-site
18
+ cd my-blocklify-site
19
+ bun add @blocklify/preview
20
+ cp -R node_modules/@blocklify/preview/starter/nextjs/app ./app
21
+ cp node_modules/@blocklify/preview/starter/nextjs/.env.example ./.env.local
22
+ ```
23
+
24
+ Fill in `.env.local`:
25
+
26
+ ```bash
27
+ BLOCKLIFY_API_URL=https://localhost:3000
28
+ NEXT_PUBLIC_BLOCKLIFY_API_URL=https://localhost:3000
29
+ BLOCKLIFY_ACCESS_TOKEN=blocklify_…
30
+ ```
31
+
32
+ Run the site on port 3001 and register `http://localhost:3001/preview` under Space → Settings → Live preview URLs.
33
+
34
+ `app/page.tsx` renders the published `home` entry. `app/preview/page.tsx` is the editor iframe.
35
+
36
+ ## New React (Vite) site
37
+
38
+ ```bash
39
+ bunx create-vite@latest my-blocklify-site --template react-ts
40
+ cd my-blocklify-site
41
+ bun add @blocklify/preview react-router-dom
42
+ cp node_modules/@blocklify/preview/starter/react/src/App.tsx ./src/App.tsx
43
+ cp node_modules/@blocklify/preview/starter/react/src/main.tsx ./src/main.tsx
44
+ mkdir -p src/pages
45
+ cp node_modules/@blocklify/preview/starter/react/src/pages/PreviewPage.tsx ./src/pages/PreviewPage.tsx
46
+ cp node_modules/@blocklify/preview/starter/react/.env.example ./.env
47
+ ```
48
+
49
+ Register `http://localhost:5173/preview` as the live preview URL.
50
+
51
+ ## What the starter renders
52
+
53
+ Matches a Blocklify starter space:
54
+
55
+ | Block type | Fields |
56
+ | ---------- | ---------------------------------------------------------- |
57
+ | `hero` | `headline`, `subheadline`, `image`, `cta_text`, `cta_link` |
58
+ | `text` | `content` (HTML) |
59
+ | `image` | `image`, `caption`, `alt` |
60
+ | `cta` | `headline`, `subtext`, `button_text`, `button_link` |
61
+
62
+ `hero_block`, `text_block`, and `image_block` use the same components.
63
+
64
+ Asset fields can be an id, a URL, or `{ url }`. Pass the Blocklify origin so ids load from `/api/assets/{id}/file`:
65
+
66
+ ```ts
67
+ import { BlocklifyEntryView, BlocklifyPreview, createStarterBlockMap } from "@blocklify/preview";
68
+
69
+ const blockMap = createStarterBlockMap({
70
+ assetBaseUrl: process.env.BLOCKLIFY_API_URL,
71
+ });
72
+ ```
20
73
 
21
- See the in-app guide at `/docs/getting-started#live-preview`.
74
+ Use `blockMap` with `BlocklifyPreview` on `/preview`, and `BlocklifyEntryView` for the public page.
package/dist/index.d.ts CHANGED
@@ -11,3 +11,4 @@
11
11
  export { createBlocklifyClient, type BlocklifyClientConfig, type BlocklifyComponent, type BlocklifyEntry, type BlocklifySiteAsset, type BlocklifySiteSettings, } from "./client.js";
12
12
  export { BlocklifyEditable, BlocklifyPreview, BlocklifyPreviewRenderer, BLOCKLIFY_PEER_FOCUS, BLOCKLIFY_PREVIEW_STYLES, BLOCKLIFY_SCROLL_TO_BLOCK, getDataBlokC, PREVIEW_EDITABLE_CSS, useBlocklifyPreview, type BlocklifyBlock, type BlocklifyBlockProps, type BlocklifyPreviewProps, type BlockMap, type DataBlokCPayload, } from "./preview.js";
13
13
  export { default } from "./preview.js";
14
+ export { BlocklifyEntryView, blocklifyAssetUrl, createStarterBlockMap, starterBlockMap, type StarterBlockMapOptions, } from "./starter.js";
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@
11
11
  export { createBlocklifyClient, } from "./client.js";
12
12
  export { BlocklifyEditable, BlocklifyPreview, BlocklifyPreviewRenderer, BLOCKLIFY_PEER_FOCUS, BLOCKLIFY_PREVIEW_STYLES, BLOCKLIFY_SCROLL_TO_BLOCK, getDataBlokC, PREVIEW_EDITABLE_CSS, useBlocklifyPreview, } from "./preview.js";
13
13
  export { default } from "./preview.js";
14
+ export { BlocklifyEntryView, blocklifyAssetUrl, createStarterBlockMap, starterBlockMap, } from "./starter.js";
@@ -0,0 +1,22 @@
1
+ import type { BlocklifyBlock, BlockMap } from "./preview.js";
2
+ export type StarterBlockMapOptions = {
3
+ /**
4
+ * Blocklify origin, no trailing slash.
5
+ * Used when an asset field is still an id (`/api/assets/{id}/file` is public).
6
+ */
7
+ assetBaseUrl?: string;
8
+ };
9
+ /** URL for an asset field: expanded `{ url }`, an absolute/path string, or a bare asset id. */
10
+ export declare function blocklifyAssetUrl(value: unknown, assetBaseUrl?: string): string | null;
11
+ /**
12
+ * Theme components for a starter Blocklify space: hero, text, image, and cta.
13
+ * Also registers the `*_block` aliases.
14
+ */
15
+ export declare function createStarterBlockMap(options?: StarterBlockMapOptions): BlockMap;
16
+ /** Starter map without an asset origin. Expanded `{ url }` fields still render. */
17
+ export declare const starterBlockMap: BlockMap;
18
+ /** Renders an entry body with a block map. Use on the public site, outside the preview iframe. */
19
+ export declare function BlocklifyEntryView({ blocks, blockMap, }: {
20
+ blocks?: BlocklifyBlock[] | null;
21
+ blockMap?: BlockMap;
22
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,139 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ function textOf(block, key) {
3
+ const value = block[key];
4
+ return typeof value === "string" ? value : "";
5
+ }
6
+ function absolutize(url, assetBaseUrl) {
7
+ if (/^https?:\/\//i.test(url) || url.startsWith("data:"))
8
+ return url;
9
+ if (url.startsWith("/") && assetBaseUrl) {
10
+ return `${assetBaseUrl.replace(/\/+$/, "")}${url}`;
11
+ }
12
+ return url;
13
+ }
14
+ /** URL for an asset field: expanded `{ url }`, an absolute/path string, or a bare asset id. */
15
+ export function blocklifyAssetUrl(value, assetBaseUrl) {
16
+ if (value == null || value === "")
17
+ return null;
18
+ if (typeof value === "object") {
19
+ const record = value;
20
+ if (typeof record.url === "string" && record.url !== "") {
21
+ return absolutize(record.url, assetBaseUrl);
22
+ }
23
+ if (typeof record.id === "string" && record.id !== "") {
24
+ return blocklifyAssetUrl(record.id, assetBaseUrl);
25
+ }
26
+ return null;
27
+ }
28
+ if (typeof value !== "string")
29
+ return null;
30
+ if (/^https?:\/\//i.test(value) || value.startsWith("/") || value.startsWith("data:")) {
31
+ return absolutize(value, assetBaseUrl);
32
+ }
33
+ if (!assetBaseUrl)
34
+ return null;
35
+ return `${assetBaseUrl.replace(/\/+$/, "")}/api/assets/${encodeURIComponent(value)}/file`;
36
+ }
37
+ const sectionStyle = {
38
+ padding: "4rem 1.5rem",
39
+ maxWidth: "42rem",
40
+ margin: "0 auto",
41
+ };
42
+ function HeroBlock({ block, BlocklifyEditable = {}, assetBaseUrl, }) {
43
+ const headline = textOf(block, "headline");
44
+ const subheadline = textOf(block, "subheadline");
45
+ const ctaText = textOf(block, "cta_text");
46
+ const ctaLink = textOf(block, "cta_link") || "#";
47
+ const imageSrc = blocklifyAssetUrl(block.image, assetBaseUrl);
48
+ return (_jsxs("section", { style: { ...sectionStyle, textAlign: "center" }, ...BlocklifyEditable, children: [imageSrc ? (
49
+ // Themes install this package outside the Blocklify app, so they cannot use its Image component.
50
+ _jsx("img", { src: imageSrc, alt: textOf(block, "alt") || headline, style: { width: "100%", borderRadius: 12, marginBottom: "1.5rem" } })) : null, headline ? (_jsx("h1", { style: { fontSize: "2.5rem", lineHeight: 1.15, margin: 0 }, children: headline })) : null, subheadline ? (_jsx("p", { style: { marginTop: "1rem", fontSize: "1.125rem", opacity: 0.75 }, children: subheadline })) : null, ctaText ? (_jsx("a", { href: ctaLink, style: {
51
+ display: "inline-block",
52
+ marginTop: "1.5rem",
53
+ padding: "0.75rem 1.5rem",
54
+ borderRadius: 999,
55
+ background: "#111",
56
+ color: "#fff",
57
+ textDecoration: "none",
58
+ fontSize: "0.875rem",
59
+ }, children: ctaText })) : null] }));
60
+ }
61
+ function TextBlock({ block, BlocklifyEditable = {} }) {
62
+ const html = textOf(block, "content") || textOf(block, "text") || textOf(block, "html");
63
+ return (_jsx("section", { style: sectionStyle, ...BlocklifyEditable, children: html ? (_jsx("div", { dangerouslySetInnerHTML: { __html: html } })) : (_jsx("p", { style: { fontStyle: "italic", opacity: 0.6 }, children: "Empty text block" })) }));
64
+ }
65
+ function ImageBlock({ block, BlocklifyEditable = {}, assetBaseUrl, }) {
66
+ const src = blocklifyAssetUrl(block.image, assetBaseUrl);
67
+ const caption = textOf(block, "caption");
68
+ const alt = textOf(block, "alt") || caption;
69
+ return (_jsxs("figure", { style: { ...sectionStyle, paddingTop: "1.5rem", paddingBottom: "1.5rem" }, ...BlocklifyEditable, children: [src ? (_jsx("img", { src: src, alt: alt, style: { width: "100%", borderRadius: 12 } })) : (_jsx("div", { style: {
70
+ padding: "3rem 1rem",
71
+ textAlign: "center",
72
+ border: "1px dashed #cbd5e1",
73
+ borderRadius: 12,
74
+ opacity: 0.7,
75
+ }, children: "No image selected" })), caption ? (_jsx("figcaption", { style: {
76
+ marginTop: "0.5rem",
77
+ textAlign: "center",
78
+ fontSize: "0.875rem",
79
+ opacity: 0.7,
80
+ }, children: caption })) : null] }));
81
+ }
82
+ function CtaBlock({ block, BlocklifyEditable = {} }) {
83
+ const headline = textOf(block, "headline");
84
+ const subtext = textOf(block, "subtext");
85
+ const buttonText = textOf(block, "button_text");
86
+ const buttonLink = textOf(block, "button_link") || "#";
87
+ return (_jsxs("section", { style: { ...sectionStyle, textAlign: "center" }, ...BlocklifyEditable, children: [headline ? _jsx("h2", { style: { fontSize: "1.75rem", margin: 0 }, children: headline }) : null, subtext ? _jsx("p", { style: { marginTop: "0.75rem", opacity: 0.75 }, children: subtext }) : null, buttonText ? (_jsx("a", { href: buttonLink, style: {
88
+ display: "inline-block",
89
+ marginTop: "1.5rem",
90
+ padding: "0.75rem 1.5rem",
91
+ borderRadius: 999,
92
+ background: "#65a30d",
93
+ color: "#fff",
94
+ textDecoration: "none",
95
+ }, children: buttonText })) : null] }));
96
+ }
97
+ function withAssetBase(Component, assetBaseUrl) {
98
+ function StarterBlock(props) {
99
+ return _jsx(Component, { ...props, assetBaseUrl: assetBaseUrl });
100
+ }
101
+ StarterBlock.displayName = Component.displayName ?? Component.name;
102
+ return StarterBlock;
103
+ }
104
+ /**
105
+ * Theme components for a starter Blocklify space: hero, text, image, and cta.
106
+ * Also registers the `*_block` aliases.
107
+ */
108
+ export function createStarterBlockMap(options) {
109
+ const assetBaseUrl = options?.assetBaseUrl;
110
+ const hero = withAssetBase(HeroBlock, assetBaseUrl);
111
+ const text = TextBlock;
112
+ const image = withAssetBase(ImageBlock, assetBaseUrl);
113
+ const cta = CtaBlock;
114
+ return {
115
+ hero,
116
+ hero_block: hero,
117
+ text,
118
+ text_block: text,
119
+ image,
120
+ image_block: image,
121
+ cta,
122
+ };
123
+ }
124
+ /** Starter map without an asset origin. Expanded `{ url }` fields still render. */
125
+ export const starterBlockMap = createStarterBlockMap();
126
+ function resolveBlock(blockMap, type) {
127
+ return blockMap[type] ?? (type.endsWith("_block") ? blockMap[type.slice(0, -6)] : undefined);
128
+ }
129
+ /** Renders an entry body with a block map. Use on the public site, outside the preview iframe. */
130
+ export function BlocklifyEntryView({ blocks, blockMap = starterBlockMap, }) {
131
+ if (!blocks?.length)
132
+ return null;
133
+ return (_jsx(_Fragment, { children: blocks.map((block, index) => {
134
+ const Component = resolveBlock(blockMap, block.type);
135
+ if (!Component)
136
+ return null;
137
+ return (_jsx(Component, { block: block, blockMap: blockMap }, block._uid ?? `${block.type}-${index}`));
138
+ }) }));
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklify/preview",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Blocklify frontend SDK: Content API client and live-preview React helpers for theme blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,10 +19,15 @@
19
19
  "./preview": {
20
20
  "types": "./dist/preview.d.ts",
21
21
  "import": "./dist/preview.js"
22
+ },
23
+ "./starter": {
24
+ "types": "./dist/starter.d.ts",
25
+ "import": "./dist/starter.js"
22
26
  }
23
27
  },
24
28
  "files": [
25
29
  "dist",
30
+ "starter",
26
31
  "README.md"
27
32
  ],
28
33
  "publishConfig": {
@@ -30,8 +35,7 @@
30
35
  },
31
36
  "repository": {
32
37
  "type": "git",
33
- "url": "git+https://github.com/BodhiMooij/blocklify.git",
34
- "directory": "packages/preview"
38
+ "url": "git+https://github.com/BodhiMooij/blocklify-preview.git"
35
39
  },
36
40
  "scripts": {
37
41
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,7 @@
1
+ # Blocklify CMS origin (no trailing slash)
2
+ BLOCKLIFY_API_URL=https://localhost:3000
3
+ # Same origin, exposed to the preview page so asset ids can load in the browser
4
+ NEXT_PUBLIC_BLOCKLIFY_API_URL=https://localhost:3000
5
+
6
+ # Read token from Admin → Access tokens
7
+ BLOCKLIFY_ACCESS_TOKEN=blocklify_your_token_here
@@ -0,0 +1,12 @@
1
+ # Next.js starter files
2
+
3
+ Copy these into a new App Router app after `bun add @blocklify/preview`.
4
+
5
+ ```bash
6
+ cp -R node_modules/@blocklify/preview/starter/nextjs/app ./app
7
+ cp node_modules/@blocklify/preview/starter/nextjs/.env.example ./.env.local
8
+ ```
9
+
10
+ Then set `BLOCKLIFY_API_URL`, `NEXT_PUBLIC_BLOCKLIFY_API_URL`, and `BLOCKLIFY_ACCESS_TOKEN` in `.env.local`.
11
+
12
+ Register `http://localhost:3001/preview` under Space → Settings → Live preview URLs, and run this app on port 3001.
@@ -0,0 +1,25 @@
1
+ import {
2
+ BlocklifyEntryView,
3
+ createBlocklifyClient,
4
+ createStarterBlockMap,
5
+ } from "@blocklify/preview";
6
+
7
+ const blockMap = createStarterBlockMap({
8
+ assetBaseUrl: process.env.BLOCKLIFY_API_URL,
9
+ });
10
+
11
+ const client = createBlocklifyClient({
12
+ baseUrl: process.env.BLOCKLIFY_API_URL!,
13
+ accessToken: process.env.BLOCKLIFY_ACCESS_TOKEN!,
14
+ });
15
+
16
+ export default async function HomePage() {
17
+ const entry = await client.getEntry("home");
18
+ const body = entry?.content.body ?? [];
19
+
20
+ return (
21
+ <main>
22
+ <BlocklifyEntryView blocks={body} blockMap={blockMap} />
23
+ </main>
24
+ );
25
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { BlocklifyPreview, createStarterBlockMap } from "@blocklify/preview";
4
+
5
+ const blockMap = createStarterBlockMap({
6
+ assetBaseUrl: process.env.NEXT_PUBLIC_BLOCKLIFY_API_URL,
7
+ });
8
+
9
+ export default function PreviewPage() {
10
+ return (
11
+ <BlocklifyPreview
12
+ blockMap={blockMap}
13
+ blocksFieldName="body"
14
+ emptyMessage="Waiting for content from the Blocklify editor…"
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,2 @@
1
+ VITE_BLOCKLIFY_API_URL=https://localhost:3000
2
+ VITE_BLOCKLIFY_ACCESS_TOKEN=blocklify_your_token_here
@@ -0,0 +1,13 @@
1
+ # React (Vite) starter files
2
+
3
+ Copy these into a Vite React app after `bun add @blocklify/preview react-router-dom`.
4
+
5
+ ```bash
6
+ cp node_modules/@blocklify/preview/starter/react/src/App.tsx ./src/App.tsx
7
+ cp node_modules/@blocklify/preview/starter/react/src/main.tsx ./src/main.tsx
8
+ mkdir -p src/pages
9
+ cp node_modules/@blocklify/preview/starter/react/src/pages/PreviewPage.tsx ./src/pages/PreviewPage.tsx
10
+ cp node_modules/@blocklify/preview/starter/react/.env.example ./.env
11
+ ```
12
+
13
+ Register `http://localhost:5173/preview` under Space → Settings → Live preview URLs.
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ BlocklifyEntryView,
4
+ createBlocklifyClient,
5
+ createStarterBlockMap,
6
+ } from "@blocklify/preview";
7
+ import type { BlocklifyBlock } from "@blocklify/preview";
8
+
9
+ const blockMap = createStarterBlockMap({
10
+ assetBaseUrl: import.meta.env.VITE_BLOCKLIFY_API_URL,
11
+ });
12
+
13
+ const client = createBlocklifyClient({
14
+ baseUrl: import.meta.env.VITE_BLOCKLIFY_API_URL,
15
+ accessToken: import.meta.env.VITE_BLOCKLIFY_ACCESS_TOKEN,
16
+ });
17
+
18
+ export default function App() {
19
+ const [blocks, setBlocks] = useState<BlocklifyBlock[]>([]);
20
+
21
+ useEffect(() => {
22
+ let cancelled = false;
23
+ client.getEntry("home").then((entry) => {
24
+ if (!cancelled) setBlocks(entry?.content.body ?? []);
25
+ });
26
+ return () => {
27
+ cancelled = true;
28
+ };
29
+ }, []);
30
+
31
+ return (
32
+ <main>
33
+ <BlocklifyEntryView blocks={blocks} blockMap={blockMap} />
34
+ </main>
35
+ );
36
+ }
@@ -0,0 +1,16 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { BrowserRouter, Route, Routes } from "react-router-dom";
4
+ import App from "./App";
5
+ import { PreviewPage } from "./pages/PreviewPage";
6
+
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <BrowserRouter>
10
+ <Routes>
11
+ <Route path="/" element={<App />} />
12
+ <Route path="/preview" element={<PreviewPage />} />
13
+ </Routes>
14
+ </BrowserRouter>
15
+ </StrictMode>
16
+ );
@@ -0,0 +1,15 @@
1
+ import { BlocklifyPreview, createStarterBlockMap } from "@blocklify/preview";
2
+
3
+ const blockMap = createStarterBlockMap({
4
+ assetBaseUrl: import.meta.env.VITE_BLOCKLIFY_API_URL,
5
+ });
6
+
7
+ export function PreviewPage() {
8
+ return (
9
+ <BlocklifyPreview
10
+ blockMap={blockMap}
11
+ blocksFieldName="body"
12
+ emptyMessage="Waiting for content from the Blocklify editor…"
13
+ />
14
+ );
15
+ }