From 97b0c22ba509a5aaa07e5709bcff0a19c10f8466 Mon Sep 17 00:00:00 2001 From: ncthuc2004 Date: Sun, 24 May 2026 18:38:19 +0700 Subject: [PATCH] Render unsupported Notion block errors --- README.md | 15 +++++++++ src/block.tsx | 59 +++++++++++++++++++++++++++++----- src/renderer.tsx | 5 ++- src/styles.css | 10 ++++++ src/types.ts | 4 +++ src/unsupported-block.test.tsx | 59 ++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 src/unsupported-block.test.tsx diff --git a/README.md b/README.md index 2d26f27..9ec1448 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,21 @@ Most common block types are supported. We happily accept pull requests to add su | Checkbox | ❌ Missing | Supported by [react-notion-x](https://github.com/NotionX/react-notion-x) | | Table Of Contents | ❌ Missing | Supported by [react-notion-x](https://github.com/NotionX/react-notion-x) | +## Unsupported Blocks + +If a Notion page contains a block type that `react-notion` cannot render, `NotionRenderer` now renders a visible warning instead of silently outputting an empty placeholder. This makes unsupported databases, checkboxes, or future Notion block types easier to diagnose. + +```jsx + ( +

This Notion page contains an unsupported block: {blockType}

+ )} +/> +``` + +To keep the previous silent behavior, pass `showUnsupportedBlockError={false}`. + ## Block Type Specific Caveats When using a code block in your Notion page, `NotionRenderer` will use `prismjs` to detect the language of the code block. diff --git a/src/block.tsx b/src/block.tsx index 919ddf4..b8a2a92 100644 --- a/src/block.tsx +++ b/src/block.tsx @@ -7,9 +7,9 @@ import { MapPageUrl, MapImageUrl, CustomBlockComponents, - BlockValueProp, CustomDecoratorComponents, - CustomDecoratorComponentProps + CustomDecoratorComponentProps, + UnsupportedBlockRenderer } from "./types"; import Asset from "./components/asset"; import Code from "./components/code"; @@ -92,6 +92,8 @@ interface Block { fullPage?: boolean; hideHeader?: boolean; + showUnsupportedBlockError?: boolean; + renderUnsupportedBlock?: UnsupportedBlockRenderer; customBlockComponents?: CustomBlockComponents; customDecoratorComponents?: CustomDecoratorComponents; } @@ -106,11 +108,34 @@ export const Block: React.FC = props => { blockMap, mapPageUrl, mapImageUrl, + showUnsupportedBlockError = true, + renderUnsupportedBlock, customBlockComponents, customDecoratorComponents } = props; const blockValue = block?.value; + const renderUnsupportedBlockError = (blockType: string) => { + if (renderUnsupportedBlock) { + return renderUnsupportedBlock(blockType, block); + } + + if (!showUnsupportedBlockError) { + return
; + } + + return ( +
+ Unsupported Notion block: {blockType} + + {" "} + This block type cannot be rendered by react-notion. Remove it from the + Notion page or pass renderUnsupportedBlock to customize this message. + +
+ ); + }; + const renderComponent = () => { const renderChildText = createRenderChildText(customDecoratorComponents); @@ -360,9 +385,20 @@ export const Block: React.FC = props => { ); case "collection_view": - if (!block) return null; + if (!block.collection) { + return renderUnsupportedBlockError("collection_view"); + } + + const collectionView = block.collection.types[0]; - const collectionView = block?.collection?.types[0]; + if ( + !collectionView || + !["table", "gallery"].includes(collectionView.type) + ) { + return renderUnsupportedBlockError( + `collection_view:${collectionView?.type || "unknown"}` + ); + } return (
@@ -516,10 +552,15 @@ export const Block: React.FC = props => { ); default: + const unsupportedType = String( + (blockValue as { type?: string } | undefined)?.type || "unknown" + ); + if (process.env.NODE_ENV !== "production") { - console.log("Unsupported type " + block?.value?.type); + console.warn("Unsupported Notion block type " + unsupportedType); } - return
; + + return renderUnsupportedBlockError(unsupportedType); } return null; }; @@ -531,12 +572,14 @@ export const Block: React.FC = props => { // Do not use custom component for base page block level !== 0 ) { - const CustomComponent = customBlockComponents[blockValue?.type]!; + const CustomComponent = customBlockComponents[ + blockValue?.type + ]! as React.FC; return ( } + blockValue={blockValue} level={level} > {children} diff --git a/src/renderer.tsx b/src/renderer.tsx index 8da93e3..fed0036 100644 --- a/src/renderer.tsx +++ b/src/renderer.tsx @@ -4,7 +4,8 @@ import { MapPageUrl, MapImageUrl, CustomBlockComponents, - CustomDecoratorComponents + CustomDecoratorComponents, + UnsupportedBlockRenderer } from "./types"; import { Block } from "./block"; import { defaultMapImageUrl, defaultMapPageUrl } from "./utils"; @@ -15,6 +16,8 @@ export interface NotionRendererProps { hideHeader?: boolean; mapPageUrl?: MapPageUrl; mapImageUrl?: MapImageUrl; + showUnsupportedBlockError?: boolean; + renderUnsupportedBlock?: UnsupportedBlockRenderer; currentId?: string; level?: number; diff --git a/src/styles.css b/src/styles.css index da0e210..2319efb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -681,3 +681,13 @@ img.notion-nav-icon { margin: 0 2px; color: rgba(55, 53, 47, 0.4); } +.notion-unsupported-block { + margin: 0.5rem 0; + padding: 0.75rem 1rem; + border-left: 3px solid #d9822b; + border-radius: 3px; + background: #fff4e5; + color: #7a4b00; + font-size: 0.95rem; + line-height: 1.4; +} diff --git a/src/types.ts b/src/types.ts index d721ea5..d990a09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -342,6 +342,10 @@ export interface LoadPageChunkData { export type MapPageUrl = (pageId: string) => string; export type MapImageUrl = (image: string, block?: BlockType) => string; +export type UnsupportedBlockRenderer = ( + blockType: string, + block: BlockType +) => JSX.Element | null; export type BlockValueProp = Extract; diff --git a/src/unsupported-block.test.tsx b/src/unsupported-block.test.tsx new file mode 100644 index 0000000..d95aebb --- /dev/null +++ b/src/unsupported-block.test.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { NotionRenderer } from "./renderer"; +import { BlockMapType } from "./types"; + +const createBlockMap = (type: string): BlockMapType => + (({ + root: { + role: "reader", + value: { + id: "root", + version: 1, + type, + created_time: 0, + last_edited_time: 0, + parent_id: "", + parent_table: "space", + alive: true, + created_by_table: "notion_user", + created_by_id: "user", + last_edited_by_table: "notion_user", + last_edited_by_id: "user" + } + } + } as unknown) as BlockMapType); + +describe("unsupported Notion blocks", () => { + it("renders a visible error for unsupported block types", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).toContain("Unsupported Notion block: collection_view_page"); + }); + + it("allows unsupported block rendering to be customized", () => { + const html = renderToStaticMarkup( + ( +

Custom unsupported block: {blockType}

+ )} + /> + ); + + expect(html).toContain("Custom unsupported block: unsupported_checkbox"); + }); + + it("can keep the previous silent placeholder behavior", () => { + const html = renderToStaticMarkup( + + ); + + expect(html).not.toContain("Unsupported Notion block"); + }); +});