@notionhq/custom-blocks 0.1.2 → 0.1.4

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/HOST.md CHANGED
@@ -73,6 +73,8 @@ Initialization has three steps. A sandbox-generated `initializationId` identifie
73
73
  { type: "connect", initializationId, status: "success", bridgeProtocolVersion, sdkVersion, manifest: { /* ... */ } }
74
74
  ```
75
75
 
76
+ The `manifest` property is optional for `connect` messages. If provided, hosts can ignore it and still return a different `manifest` in the `init` message. This sandbox-defined manifest option is included for locally-hosted custom blocks.
77
+
76
78
  If `connect.status` is `"error"`, reply with `init.error` using the same `initializationId` and forward its complete error payload unchanged.
77
79
 
78
80
  #### 2. Host sends `init`
@@ -181,18 +183,18 @@ Schemas:
181
183
 
182
184
  Messages sent from the sandbox to the host. Parse `window` `message` events with `sandboxToHostMessageSchema` (or per-message schemas). The type / schema column points to the payload shape.
183
185
 
184
- | Wire type | Type / schema | Behavior |
185
- | -------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
- | `connect` | `ConnectMessage` / `connectMessageSchema` | Starts the one-shot handshake with a sandbox-generated `initializationId`; carries `bridgeProtocolVersion`, `sdkVersion`, and the manifest. |
187
- | `initResult` | `InitResultMessage` / `initResultMessageSchema` | Echoes the handshake `initializationId` and reports whether the sandbox applied `init.success`. |
188
- | `queryDataSource` | `QueryDataSourceMessage` / `queryDataSourceMessageSchema` | Starts or updates a `subscriptionId`-keyed subscription for rows in a raw `dataSourceId`. Later results for the same subscription replace its current value. |
189
- | `createPage` | `CreatePageMessage` / `createPageMessageSchema` | `requestId`-keyed page creation with properties and optional position; parent is `page_id` or `data_source_id`. Create-time icon and cover are not supported. |
190
- | `getPage` | `GetPageMessage` / `getPageMessageSchema` | `requestId`-keyed page fetch by page id. |
191
- | `updatePage` | `UpdatePageMessage` / `updatePageMessageSchema` | `requestId`-keyed patch (properties, icon, cover, or `archived`). |
192
- | `getUser` | `GetUserMessage` / `getUserMessageSchema` | `requestId`-keyed user fetch by user id. |
193
- | `listUsers` | `ListUsersMessage` / `listUsersMessageSchema` | `requestId`-keyed user list with optional `startCursor` and `pageSize`. |
194
- | `resize` | `ResizeMessage` / `resizeMessageSchema` | Latest measured content height from auto-resize. |
195
- | `invalidHostMessage` | `InvalidHostMessage` / `invalidHostMessageSchema` | Sandbox-side NACK for a host message it could not parse. |
186
+ | Wire type | Type / schema | Behavior |
187
+ | -------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
188
+ | `connect` | `ConnectMessage` / `connectMessageSchema` | Starts the one-shot handshake with a sandbox-generated `initializationId`; carries `bridgeProtocolVersion`, `sdkVersion`, and the manifest. |
189
+ | `initResult` | `InitResultMessage` / `initResultMessageSchema` | Echoes the handshake `initializationId` and reports whether the sandbox applied `init.success`. |
190
+ | `queryDataSource` | `QueryDataSourceMessage` / `queryDataSourceMessageSchema` | Starts or updates a `subscriptionId`-keyed subscription for rows in a raw `dataSourceId`. Later results for the same subscription replace its current value. |
191
+ | `createPage` | `CreatePageMessage` / `createPageMessageSchema` | `requestId`-keyed page creation with properties and optional position; parent is `page_id` or `data_source_id`. Create-time icon and cover are not supported. |
192
+ | `getPage` | `GetPageMessage` / `getPageMessageSchema` | `requestId`-keyed page fetch by page id. |
193
+ | `updatePage` | `UpdatePageMessage` / `updatePageMessageSchema` | `requestId`-keyed patch (properties, icon, cover, or `archived`). |
194
+ | `getUser` | `GetUserMessage` / `getUserMessageSchema` | `requestId`-keyed user fetch by user id. |
195
+ | `listUsers` | `ListUsersMessage` / `listUsersMessageSchema` | `requestId`-keyed user list with optional `startCursor` and `pageSize`. |
196
+ | `resize` | `ResizeMessage` / `resizeMessageSchema` | Latest measured content height from auto-resize. |
197
+ | `invalidHostMessage` | `InvalidHostMessage` / `invalidHostMessageSchema` | Sandbox-side NACK for a host message it could not parse. |
196
198
 
197
199
  `SandboxToHostMessage` / `sandboxToHostMessageSchema` is the discriminated union over all of the above.
198
200
 
@@ -209,13 +211,13 @@ Messages sent from the host to the sandbox. Same `{ wire type, type / schema, be
209
211
  | `pageChanged` | `PageChangedMessage` / `pageChangedMessageSchema` | Replaces the nearest page ancestor without disturbing theme, block ID, parent, user, or query state. |
210
212
  | `currentUserChanged` | `CurrentUserChangedMessage` / `currentUserChangedMessageSchema` | Replaces the current viewer record. Send when any viewer field changes (name, avatar, email). |
211
213
  | `dataSourcesChanged` | `DataSourcesChangedMessage` / `dataSourcesChangedMessageSchema` | Replaces data-source bindings; the sandbox preserves cached query state for keys that still exist and drops removed keys. |
212
- | `queryDataSourceResult` | `QueryDataSourceResultMessage` / `queryDataSourceResultMessageSchema` | `subscriptionId`-keyed update with `status: "success"`, `items`, and `hasMore`, or `status: "error"` and `error`. |
214
+ | `queryDataSourceResult` | `QueryDataSourceResultMessage` / `queryDataSourceResultMessageSchema` | `subscriptionId`-keyed update with `status: "success"`, `items`, and `hasMore`, or `status: "error"` and `error`. |
213
215
  | `createPageResult` | `CreatePageResultMessage` / `createPageResultMessageSchema` | `requestId`-keyed page response with `status: "success"` or `status: "error"`. |
214
216
  | `getPageResult` | `GetPageResultMessage` / `getPageResultMessageSchema` | Same success/error shape as `createPageResult`. |
215
217
  | `updatePageResult` | `UpdatePageResultMessage` / `updatePageResultMessageSchema` | Same success/error shape as `createPageResult`. |
216
218
  | `getUserResult` | `GetUserResultMessage` / `getUserResultMessageSchema` | `requestId`-keyed user response with `status: "success"` or `status: "error"`. |
217
219
  | `listUsersResult` | `ListUsersResultMessage` / `listUsersResultMessageSchema` | Same success/error shape as `getUserResult`. |
218
- | `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse. |
220
+ | `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse. |
219
221
 
220
222
  `HostToSandboxMessage` / `hostToSandboxMessageSchema` is the discriminated union over all of the above.
221
223
 
package/README.md CHANGED
@@ -1,18 +1,33 @@
1
1
  # @notionhq/custom-blocks
2
2
 
3
3
  > [!NOTE]
4
- > Nothing to see here. This is **extreme** alpha and currently only works for an unreleased product.
4
+ > **Pre-release.** Custom blocks are in private alpha. Breaking changes may land at any time.
5
5
 
6
6
  SDK for building Notion custom blocks.
7
7
 
8
- A custom block runs as a sandboxed `<iframe>` inside a Notion block on iOS, Android, and desktop. The only channel between your block and Notion is a `postMessage` bridge this SDK wraps it in typed React hooks plus a small framework-neutral runtime API for non-React renderers.
9
-
10
- > **Pre-release.** Breaking changes may land at any time before 1.0.
8
+ A custom block runs as a sandboxed `<iframe>` inside the Notion app that has no direct access to the internet. The only channel between your block and Notion is a local `postMessage` bridge. This library implements the sandbox side of the bridge protocol and wraps it in a framework-neutral TypeScript API (`@notionhq/custom-blocks`) and typed React hooks (`@notionhq/custom-blocks/react`).
11
9
 
12
10
  ## Quick start
13
11
 
12
+ Declare your custom block along with the rest of your worker definition:
13
+
14
+ ```ts
15
+ // src/index.ts
16
+ import { Worker } from "@notionhq/workers";
17
+
18
+ const worker = new Worker();
19
+ export default worker;
20
+
21
+ worker.customBlock("hello", {
22
+ path: "./blocks/hello",
23
+ command: "npx vite build",
24
+ });
25
+ ```
26
+
27
+ In the block's entry point defined above, wrap your React code in `<NotionCustomBlock>` so the SDK connects with Notion before anything renders:
28
+
14
29
  ```tsx
15
- // src/index.tsx
30
+ // blocks/hello/src/index.tsx
16
31
  import "@notionhq/custom-blocks/nds.css";
17
32
  import {
18
33
  NotionCustomBlock,
@@ -30,8 +45,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
30
45
  );
31
46
  ```
32
47
 
48
+ Hooks can then read live content directly from Notion:
49
+
33
50
  ```tsx
34
- // src/App.tsx
51
+ // blocks/hello/src/App.tsx
35
52
  import { useBlockId } from "@notionhq/custom-blocks/react";
36
53
 
37
54
  export function App() {
@@ -72,9 +89,8 @@ API surface, one page per category. Import framework-neutral APIs from `@notionh
72
89
  - [`docs/pages.md`](./docs/pages.md) — `pages.create / get / update / delete`, parent variants (including the recommended `data_source_key`), property input shapes.
73
90
  - [`docs/users.md`](./docs/users.md) — `users.list / get`, the `NotionUser` shape, paging.
74
91
  - [`docs/errors.md`](./docs/errors.md) — request results, error format, error codes, retries, and initialization failures.
75
- - [`docs/vite-plugin.md`](./docs/vite-plugin.md) — the `notionCustomBlock()` Vite plugin and `custom_blocks.json`, the block's local-preview manifest.
76
-
77
- Declare the block source and data-source schema in the worker file with `worker.customBlock(...)`, then build and deploy it with `ntn workers deploy`. The worker definition is the source of truth; the iframe frontend uses semantic keys from that definition with APIs such as `useDataSource`. For local previews, the Vite plugin serves the block's `custom_blocks.json` — see [`docs/vite-plugin.md`](./docs/vite-plugin.md).
92
+ - [`docs/deployment.md`](./docs/deployment.md) — worker-backed deploys, localhost self-hosted fallback, where the manifest comes from.
93
+ - [`docs/vite-plugin.md`](./docs/vite-plugin.md) — the `notionCustomBlock()` Vite plugin.
78
94
 
79
95
  ## Forbidden APIs
80
96
 
@@ -1,7 +1,7 @@
1
1
  import type { CustomBlockErrorInfo } from "./errors.js";
2
2
  import { type CustomBlockManifest } from "./manifest.js";
3
- export type CustomBlockConnectErrorCode = "manifest_unavailable" | "manifest_invalid" | (string & {});
4
- export type CustomBlockConnectError = CustomBlockErrorInfo<CustomBlockConnectErrorCode>;
3
+ type CustomBlockConnectErrorCode = "manifest_unavailable" | "manifest_invalid" | (string & {});
4
+ type CustomBlockConnectError = CustomBlockErrorInfo<CustomBlockConnectErrorCode>;
5
5
  export type ManifestLoadResult = {
6
6
  manifest: CustomBlockManifest | null;
7
7
  error?: undefined;
@@ -10,11 +10,12 @@ export type ManifestLoadResult = {
10
10
  error: CustomBlockConnectError;
11
11
  };
12
12
  /**
13
- * Attempts to load a `custom_blocks.json` manifest co-located with the bundle.
14
- * A missing manifest means the block has no declared data requirements.
15
- * Other failures return a structured error so the host can reject the manifest.
16
- * The SDK validates the manifest locally for author feedback; the host still
17
- * validates the connect message as the iframe trust boundary.
13
+ * Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
14
+ * `/custom_blocks.json`. This is to support local development where the block is served from a
15
+ * local dev server.
16
+ *
17
+ * It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
18
18
  */
19
- export declare function loadManifest(): Promise<ManifestLoadResult>;
19
+ export declare function attemptToLoadSelfHostedManifest(): Promise<ManifestLoadResult>;
20
+ export {};
20
21
  //# sourceMappingURL=loadManifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAA;AAIxE,MAAM,MAAM,2BAA2B,GACpC,sBAAsB,GACtB,kBAAkB,GAClB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEhB,MAAM,MAAM,uBAAuB,GAClC,oBAAoB,CAAC,2BAA2B,CAAC,CAAA;AAElD,MAAM,MAAM,kBAAkB,GAC3B;IACA,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;CAChB,GACD;IACA,QAAQ,EAAE,IAAI,CAAA;IACd,KAAK,EAAE,uBAAuB,CAAA;CAC7B,CAAA;AAEJ;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAkEhE"}
1
+ {"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAA;AAIxE,KAAK,2BAA2B,GAC7B,sBAAsB,GACtB,kBAAkB,GAClB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEhB,KAAK,uBAAuB,GAAG,oBAAoB,CAAC,2BAA2B,CAAC,CAAA;AAEhF,MAAM,MAAM,kBAAkB,GAC3B;IACA,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;CAChB,GACD;IACA,QAAQ,EAAE,IAAI,CAAA;IACd,KAAK,EAAE,uBAAuB,CAAA;CAC7B,CAAA;AAEJ;;;;;;GAMG;AACH,wBAAsB,+BAA+B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA0DnF"}
@@ -2,21 +2,13 @@ import * as v from "valibot";
2
2
  import { manifestSchema } from "./manifest.js";
3
3
  const MANIFEST_URL = "custom_blocks.json";
4
4
  /**
5
- * Attempts to load a `custom_blocks.json` manifest co-located with the bundle.
6
- * A missing manifest means the block has no declared data requirements.
7
- * Other failures return a structured error so the host can reject the manifest.
8
- * The SDK validates the manifest locally for author feedback; the host still
9
- * validates the connect message as the iframe trust boundary.
5
+ * Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
6
+ * `/custom_blocks.json`. This is to support local development where the block is served from a
7
+ * local dev server.
8
+ *
9
+ * It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
10
10
  */
11
- export async function loadManifest() {
12
- if (typeof fetch !== "function") {
13
- const message = `No fetch API available; cannot load ${MANIFEST_URL}.`;
14
- console.warn(`[custom-blocks-sdk] ${message}`);
15
- return {
16
- manifest: null,
17
- error: { code: "manifest_unavailable", message, isRetryable: true },
18
- };
19
- }
11
+ export async function attemptToLoadSelfHostedManifest() {
20
12
  let response;
21
13
  try {
22
14
  response = await fetch(MANIFEST_URL, { credentials: "omit" });
@@ -1,12 +1,4 @@
1
1
  import * as v from "valibot";
2
- /**
3
- * User-authored manifest declaring the data sources the custom block expects.
4
- * Lives at `custom_blocks.json` in the project root. The sandbox may send it in
5
- * `connect`, and the host returns the authoritative manifest in `init`. The
6
- * `notionCustomBlock` Vite plugin from
7
- * `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
8
- * the build output.
9
- */
10
2
  /**
11
3
  * Decorative icon attached to a manifest data source. Mirrors the
12
4
  * `emoji` / `external` icon variants the public Notion API uses, so the host
@@ -62,5 +54,8 @@ export declare const manifestSchema: v.ObjectSchema<{
62
54
  }, undefined>, undefined>, {}>;
63
55
  }, undefined>, undefined>;
64
56
  }, undefined>;
57
+ /**
58
+ * Manifest declaring the data sources required by a custom block.
59
+ */
65
60
  export type CustomBlockManifest = v.InferOutput<typeof manifestSchema>;
66
61
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAG5B;;;;;;;GAOG;AAEH;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;0BAS7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAEnE,eAAO,MAAM,sBAAsB;;;;aAIjC,CAAA;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAE3E,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;aAKnC,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAE/E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;aAGzB,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,cAAc,CAAC,CAAA"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAG5B;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;0BAS7B,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAEnE,eAAO,MAAM,sBAAsB;;;;aAIjC,CAAA;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAE3E,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;aAKnC,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,wBAAwB,CAAC,CAAA;AAE/E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;aAGzB,CAAA;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,cAAc,CAAC,CAAA"}
@@ -1,13 +1,5 @@
1
1
  import * as v from "valibot";
2
2
  import { notionPropertyTypeSchema } from "./dataSources/propertySchema.js";
3
- /**
4
- * User-authored manifest declaring the data sources the custom block expects.
5
- * Lives at `custom_blocks.json` in the project root. The sandbox may send it in
6
- * `connect`, and the host returns the authoritative manifest in `init`. The
7
- * `notionCustomBlock` Vite plugin from
8
- * `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
9
- * the build output.
10
- */
11
3
  /**
12
4
  * Decorative icon attached to a manifest data source. Mirrors the
13
5
  * `emoji` / `external` icon variants the public Notion API uses, so the host
package/dist/init.d.ts CHANGED
@@ -41,15 +41,14 @@ export type InitCustomBlockOptions = {
41
41
  timeoutMs?: number;
42
42
  };
43
43
  /**
44
- * Performs the SDK <-> host handshake: loads `custom_blocks.json`, posts
45
- * `connect`, then awaits the host's `init` message. Resolves after the sandbox
46
- * applies the payload and returns `initResult.success`.
44
+ * Initializes the custom block by running the SDK <-> host handshake.
47
45
  *
48
- * Rejects with a `CustomBlockInitializationError` if initialization fails.
46
+ * Resolves with the block's initial context (theme, block location, current
47
+ * user, data sources) once the host has initialized the block. Rejects with a
48
+ * `CustomBlockInitializationError` if initialization fails.
49
49
  *
50
50
  * Idempotent: subsequent calls return the same promise as the first and ignore any new options.
51
- * Mount your React tree (or call any SDK hook / `customBlock.subscribe`) only after the
52
- * returned promise resolves.
51
+ * Mount your React tree (or call any SDK hook) only after the returned promise resolves.
53
52
  */
54
53
  export declare function initCustomBlock(opts?: InitCustomBlockOptions): Promise<CustomBlockInitPayload>;
55
54
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGxD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,gBAAgB,EAAE,CAAA;CAC/B,CAAA;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,8BAA8B;gBACvD,OAAO,GAAE,MAA8B;IAS3C,IAAI,EAAE,eAAe,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC9B,IAAI,GAAE,sBAA2B,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAKjC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAA;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,2BAA2B,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AAGxD;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,kBAAkB,CAAA;IAChC,OAAO,EAAE,aAAa,CAAA;IACtB,MAAM,EAAE,YAAY,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,WAAW,EAAE,UAAU,CAAA;IACvB,WAAW,EAAE,gBAAgB,EAAE,CAAA;CAC/B,CAAA;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,8BAA8B;gBACvD,OAAO,GAAE,MAA8B;IAS3C,IAAI,EAAE,eAAe,CAAA;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACpC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AASD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC9B,IAAI,GAAE,sBAA2B,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAKjC"}
package/dist/init.js CHANGED
@@ -1,4 +1,4 @@
1
- import { loadManifest } from "./bridge/loadManifest.js";
1
+ import { attemptToLoadSelfHostedManifest } from "./bridge/loadManifest.js";
2
2
  import { CustomBlockInitializationError } from "./bridge/messages/init.js";
3
3
  import { customBlockHost } from "./bridge/sandboxClient.js";
4
4
  import { unreachable } from "./utils.js";
@@ -22,23 +22,32 @@ const DEFAULT_INIT_TIMEOUT_MS = 15000;
22
22
  const NOT_IN_IFRAME_MESSAGE = "<NotionCustomBlock> only works inside an iframe — use the dev shell or deploy to Notion.";
23
23
  let initPromise;
24
24
  /**
25
- * Performs the SDK <-> host handshake: loads `custom_blocks.json`, posts
26
- * `connect`, then awaits the host's `init` message. Resolves after the sandbox
27
- * applies the payload and returns `initResult.success`.
25
+ * Initializes the custom block by running the SDK <-> host handshake.
28
26
  *
29
- * Rejects with a `CustomBlockInitializationError` if initialization fails.
27
+ * Resolves with the block's initial context (theme, block location, current
28
+ * user, data sources) once the host has initialized the block. Rejects with a
29
+ * `CustomBlockInitializationError` if initialization fails.
30
30
  *
31
31
  * Idempotent: subsequent calls return the same promise as the first and ignore any new options.
32
- * Mount your React tree (or call any SDK hook / `customBlock.subscribe`) only after the
33
- * returned promise resolves.
32
+ * Mount your React tree (or call any SDK hook) only after the returned promise resolves.
34
33
  */
35
34
  export function initCustomBlock(opts = {}) {
36
35
  if (initPromise === undefined) {
37
- initPromise = initializeCustomBlock(opts);
36
+ initPromise = performHandshake(opts);
38
37
  }
39
38
  return initPromise;
40
39
  }
41
- async function initializeCustomBlock(opts) {
40
+ /**
41
+ * Performs the host <-> sandbox SDK handshake:
42
+ * 1. Attempts to load an optional self-hosted `custom_blocks.json`
43
+ * 2. Sends `connect` with the manifest
44
+ * 3. Awaits the host's `init` message
45
+ * 4. Applies the `init` payload and acknowledges with `initResult` (fire-and-forget)
46
+ *
47
+ * Resolves with the `init` payload after the process above. Rejects with a
48
+ * `CustomBlockInitializationError` if any step fails.
49
+ */
50
+ async function performHandshake(opts) {
42
51
  try {
43
52
  // Fail fast with a typed error when rendered as a standalone tab and not in a parent frame.
44
53
  // Otherwise, it would eventually hit the timeout, since `postMessage` to `window.parent`
@@ -47,7 +56,7 @@ async function initializeCustomBlock(opts) {
47
56
  throw new NotInIframeError();
48
57
  }
49
58
  // Load the manifest and send it to the host.
50
- const manifestResult = await loadManifest();
59
+ const manifestResult = await attemptToLoadSelfHostedManifest();
51
60
  customBlockHost.sendConnect(manifestResult);
52
61
  const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS;
53
62
  await awaitHostInitWithTimeout(timeoutMs);
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  *
5
5
  * WARNING: Generated during SDK publish. Do not edit in the published package.
6
6
  */
7
- export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.2"
7
+ export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.4"
@@ -0,0 +1,41 @@
1
+ # Deployment and manifests
2
+
3
+ A custom block is a static web bundle — an `index.html` plus optional assets — that Notion loads into a sandboxed `<iframe>`, so the bundle needs to be hosted somewhere Notion can reach. Notion does that hosting: the block ships as part of a worker. Declare the block source and data-source schema with `worker.customBlock(...)` in the worker's `src/index.ts`, deploy with `ntn workers deploy`, and Notion serves the built bundle. Serving the bundle from your own dev server is provided as a local-development fallback, not an officially supported deployment target.
4
+
5
+ The difference between the two paths is where the manifest lives:
6
+
7
+ - **Worker-backed** — deploying persists the manifest on the block's definition record in Notion. The host reads it from there. The bundle carries no manifest.
8
+ - **Self-hosted (localhost)** — there is no persisted record, so the block serves its manifest dynamically at `/custom_blocks.json` and passes the manifest to the host in the `connect` handshake.
9
+
10
+ For a self-hosted block, the sandbox can propose the initial manifest in `connect`. The host decides whether to use it and sends the authoritative manifest back in `init`. The SDK always uses the host-provided manifest from `init` even if it differs from the one sent during `connect`.
11
+
12
+ ## Self-hosted blocks (localhost fallback)
13
+
14
+ During development, a Notion host can point a custom block at a local dev server instead of a deployed worker. The block describes itself with an optional `custom_blocks.json` at the bundle root:
15
+
16
+ ```json
17
+ {
18
+ "version": 1,
19
+ "dataSources": {
20
+ "tasks": {
21
+ "name": "Tasks",
22
+ "description": "The collection of tasks to render",
23
+ "properties": {
24
+ "title": { "name": "Title", "type": "title" },
25
+ "dueDate": { "name": "Due date", "type": "date" }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ At startup the SDK fetches this file from the bundle's origin and forwards it in `connect`. If the file is missing, the SDK omits `manifest` from `connect`. If it is present but unreadable or invalid, initialization fails.
33
+
34
+ The [Vite plugin](./vite-plugin.md) keeps this file working in dev and emits it into `dist/` on build.
35
+
36
+ ## Types
37
+
38
+ - `CustomBlockManifest` — the parsed shape of a block manifest.
39
+ - `ManifestDataSource` — a single entry in `dataSources`.
40
+ - `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
41
+ - `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
@@ -12,35 +12,11 @@ export default defineConfig({
12
12
  });
13
13
  ```
14
14
 
15
- In dev, the plugin serves `custom_blocks.json` from the project root so HMR and the SDK handshake see the same file. On `vite build`, it emits `custom_blocks.json` into `dist/` as a separate asset alongside the bundled HTML and JS. It also forces Vite's `base` to `"./"` (and rejects any other explicit value) — custom blocks are served from a content-addressed path, so asset URLs in the built bundle must be relative.
15
+ It does two things:
16
16
 
17
- ## `custom_blocks.json` and local previews
17
+ - **Forces `base: "./"`** (and rejects any other explicit value). Custom blocks are served from a content-addressed path, so asset URLs in the built bundle must be relative.
18
+ - **Passes through `custom_blocks.json`** when the optional file exists in the project root: served by middleware in dev so HMR and the SDK handshake see the same file, and emitted into `dist/` as a separate asset on `vite build`.
18
19
 
19
- `custom_blocks.json` is the block's local manifest the data-source keys and property shapes the block expects:
20
+ The `custom_blocks.json` manifest file only applies when a block is served from `localhost` during development instead of deployed as a worker. Worker-backed blocks declare their manifest with `worker.customBlock(...)` and should not maintain a `custom_blocks.json`, but they still use the plugin for the `base` enforcement. See [deployment.md](./deployment.md) for more information.
20
21
 
21
- ```json
22
- {
23
- "version": 1,
24
- "dataSources": {
25
- "tasks": {
26
- "name": "Tasks",
27
- "description": "The collection of tasks to render",
28
- "properties": {
29
- "title": { "name": "Title", "type": "title" },
30
- "dueDate": { "name": "Due date", "type": "date" }
31
- }
32
- }
33
- }
34
- }
35
- ```
36
-
37
- For a worker-backed block, the `worker.customBlock(...)` declaration in the worker's `src/index.ts` is the source of truth at deploy time. But local previews (pasting the Vite dev URL into a live Notion block, or the dev shell) don't read the worker declaration — the SDK fetches `custom_blocks.json` from the plugin and forwards it on `connect`, and the host binds data sources against those keys. Keep the file mirroring the worker's `dataSources` schema so previews see the same slots the deployed block will; leave it as `{ "version": 1, "dataSources": {} }` if the block reads no host data.
38
-
39
- If the file is missing, the SDK omits `manifest` from `connect`. If it is unreadable or invalid, the SDK sends `connect` with `status: "error"` and an `error` payload. The host returns its authoritative manifest in `init`, and the SDK uses that one even when it differs from what was sent in `connect`.
40
-
41
- ## Types
42
-
43
- - `CustomBlockManifest` — the parsed shape of `custom_blocks.json`.
44
- - `ManifestDataSource` — a single entry in `dataSources` (name, description, properties).
45
- - `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
46
- - `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
22
+ The plugin does not validate the manifest. The SDK validates it on receipt.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -4,13 +4,12 @@ import { type CustomBlockManifest, manifestSchema } from "./manifest.js"
4
4
 
5
5
  const MANIFEST_URL = "custom_blocks.json"
6
6
 
7
- export type CustomBlockConnectErrorCode =
7
+ type CustomBlockConnectErrorCode =
8
8
  | "manifest_unavailable"
9
9
  | "manifest_invalid"
10
10
  | (string & {})
11
11
 
12
- export type CustomBlockConnectError =
13
- CustomBlockErrorInfo<CustomBlockConnectErrorCode>
12
+ type CustomBlockConnectError = CustomBlockErrorInfo<CustomBlockConnectErrorCode>
14
13
 
15
14
  export type ManifestLoadResult =
16
15
  | {
@@ -23,21 +22,13 @@ export type ManifestLoadResult =
23
22
  }
24
23
 
25
24
  /**
26
- * Attempts to load a `custom_blocks.json` manifest co-located with the bundle.
27
- * A missing manifest means the block has no declared data requirements.
28
- * Other failures return a structured error so the host can reject the manifest.
29
- * The SDK validates the manifest locally for author feedback; the host still
30
- * validates the connect message as the iframe trust boundary.
25
+ * Attempts to load the custom block's optional self-hosted manifest, co-located with the bundle at
26
+ * `/custom_blocks.json`. This is to support local development where the block is served from a
27
+ * local dev server.
28
+ *
29
+ * It is up to the host to decide whether to use this manifest or provide its own persisted manifest.
31
30
  */
32
- export async function loadManifest(): Promise<ManifestLoadResult> {
33
- if (typeof fetch !== "function") {
34
- const message = `No fetch API available; cannot load ${MANIFEST_URL}.`
35
- console.warn(`[custom-blocks-sdk] ${message}`)
36
- return {
37
- manifest: null,
38
- error: { code: "manifest_unavailable", message, isRetryable: true },
39
- }
40
- }
31
+ export async function attemptToLoadSelfHostedManifest(): Promise<ManifestLoadResult> {
41
32
  let response: Response
42
33
  try {
43
34
  response = await fetch(MANIFEST_URL, { credentials: "omit" })
@@ -1,15 +1,6 @@
1
1
  import * as v from "valibot"
2
2
  import { notionPropertyTypeSchema } from "./dataSources/propertySchema.js"
3
3
 
4
- /**
5
- * User-authored manifest declaring the data sources the custom block expects.
6
- * Lives at `custom_blocks.json` in the project root. The sandbox may send it in
7
- * `connect`, and the host returns the authoritative manifest in `init`. The
8
- * `notionCustomBlock` Vite plugin from
9
- * `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
10
- * the build output.
11
- */
12
-
13
4
  /**
14
5
  * Decorative icon attached to a manifest data source. Mirrors the
15
6
  * `emoji` / `external` icon variants the public Notion API uses, so the host
@@ -50,4 +41,7 @@ export const manifestSchema = v.object({
50
41
  dataSources: v.record(v.string(), manifestDataSourceSchema),
51
42
  })
52
43
 
44
+ /**
45
+ * Manifest declaring the data sources required by a custom block.
46
+ */
53
47
  export type CustomBlockManifest = v.InferOutput<typeof manifestSchema>
package/src/init.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { NotionContrastMode } from "./bridge/contrast.js"
2
2
  import type { NotionDataSource } from "./bridge/dataSources/dataSource.js"
3
3
  import type { NotionBlockId } from "./bridge/ids.js"
4
- import { loadManifest } from "./bridge/loadManifest.js"
4
+ import { attemptToLoadSelfHostedManifest } from "./bridge/loadManifest.js"
5
5
  import { CustomBlockInitializationError } from "./bridge/messages/init.js"
6
6
  import type { CustomBlockPage } from "./bridge/pages/page.js"
7
7
  import type { NotionParent } from "./bridge/parent.js"
@@ -63,26 +63,35 @@ const NOT_IN_IFRAME_MESSAGE =
63
63
  let initPromise: Promise<CustomBlockInitPayload> | undefined
64
64
 
65
65
  /**
66
- * Performs the SDK <-> host handshake: loads `custom_blocks.json`, posts
67
- * `connect`, then awaits the host's `init` message. Resolves after the sandbox
68
- * applies the payload and returns `initResult.success`.
66
+ * Initializes the custom block by running the SDK <-> host handshake.
69
67
  *
70
- * Rejects with a `CustomBlockInitializationError` if initialization fails.
68
+ * Resolves with the block's initial context (theme, block location, current
69
+ * user, data sources) once the host has initialized the block. Rejects with a
70
+ * `CustomBlockInitializationError` if initialization fails.
71
71
  *
72
72
  * Idempotent: subsequent calls return the same promise as the first and ignore any new options.
73
- * Mount your React tree (or call any SDK hook / `customBlock.subscribe`) only after the
74
- * returned promise resolves.
73
+ * Mount your React tree (or call any SDK hook) only after the returned promise resolves.
75
74
  */
76
75
  export function initCustomBlock(
77
76
  opts: InitCustomBlockOptions = {},
78
77
  ): Promise<CustomBlockInitPayload> {
79
78
  if (initPromise === undefined) {
80
- initPromise = initializeCustomBlock(opts)
79
+ initPromise = performHandshake(opts)
81
80
  }
82
81
  return initPromise
83
82
  }
84
83
 
85
- async function initializeCustomBlock(
84
+ /**
85
+ * Performs the host <-> sandbox SDK handshake:
86
+ * 1. Attempts to load an optional self-hosted `custom_blocks.json`
87
+ * 2. Sends `connect` with the manifest
88
+ * 3. Awaits the host's `init` message
89
+ * 4. Applies the `init` payload and acknowledges with `initResult` (fire-and-forget)
90
+ *
91
+ * Resolves with the `init` payload after the process above. Rejects with a
92
+ * `CustomBlockInitializationError` if any step fails.
93
+ */
94
+ async function performHandshake(
86
95
  opts: InitCustomBlockOptions,
87
96
  ): Promise<CustomBlockInitPayload> {
88
97
  try {
@@ -94,7 +103,7 @@ async function initializeCustomBlock(
94
103
  }
95
104
 
96
105
  // Load the manifest and send it to the host.
97
- const manifestResult = await loadManifest()
106
+ const manifestResult = await attemptToLoadSelfHostedManifest()
98
107
  customBlockHost.sendConnect(manifestResult)
99
108
 
100
109
  const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS
@@ -40,7 +40,7 @@ export type NotionCustomBlockPlugin = {
40
40
  }
41
41
 
42
42
  /**
43
- * Vite plugin that serves the project's `custom_blocks.json` to the custom-block
44
- * bundle in dev and emits it as a separate asset on build.
43
+ * Vite plugin that serves an optional self-hosted `custom_blocks.json` to the custom block
44
+ * bundle during local development and emits it as a separate asset on build.
45
45
  */
46
46
  export function notionCustomBlock(): NotionCustomBlockPlugin
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Vite plugin half of `@notionhq/custom-blocks`. Exposes the project's
3
- * `custom_blocks.json` manifest to the custom-block bundle so the SDK's runtime
4
- * fetch can resolve.
2
+ * Vite plugin half of `@notionhq/custom-blocks`. Exposes an optional
3
+ * self-hosted `custom_blocks.json` manifest to the custom-block bundle so the
4
+ * SDK's runtime fetch can resolve.
5
5
  *
6
6
  * Authored as plain JS (with `index.d.ts` co-located for types) — the rest of
7
7
  * the SDK is `.ts` consumed directly via the workspace symlink, but Vite's
@@ -21,10 +21,11 @@
21
21
  * Vite's config loader uses Node's ESM resolver (not CommonJS `require`,
22
22
  * which fails for ESM-only deps).
23
23
  *
24
- * - **Dev (`vite`)** — middleware serves `/custom_blocks.json` from the project root.
25
- * - **Build (`vite build`)** — emits `custom_blocks.json` as an asset alongside the
26
- * built `index.html` so `vite preview` and any local file server can serve
27
- * it. In production, the custom-block CLI handles serving.
24
+ * - **Dev (`vite`)** — middleware serves `/custom_blocks.json` from the project root
25
+ * when the optional file exists.
26
+ * - **Build (`vite build`)** emits an existing `custom_blocks.json` as an asset
27
+ * alongside the built `index.html` so `vite preview` and any local file server
28
+ * can serve it.
28
29
  *
29
30
  * Validation is intentionally not performed here — the SDK validates on
30
31
  * receipt, and the CLI validates separately at deploy time.