diff --git a/packages/nreact/src/index.tsx b/packages/nreact/src/index.tsx
index 256d9310..09bfada8 100644
--- a/packages/nreact/src/index.tsx
+++ b/packages/nreact/src/index.tsx
@@ -20,6 +20,7 @@ export * from './components/checkbox'
export * from './components/asset-wrapper'
export * from './components/sync-pointer-block'
export * from './components/graceful-image'
+export * from './components/lazy-image'
export * from './components/google-drive'
export * from './icons/property-icon'
export * from './third-party/collection-column-title'
diff --git a/test/components-render.test.tsx b/test/components-render.test.tsx
new file mode 100644
index 00000000..1dea56b8
--- /dev/null
+++ b/test/components-render.test.tsx
@@ -0,0 +1,164 @@
+import { describe, test, expect } from 'vitest'
+import React from '../packages/nreact/node_modules/react'
+import { renderToString } from '../packages/nreact/node_modules/react-dom/server'
+import { NotionContextProvider } from '../packages/nreact/src/context'
+import {
+ Block,
+ AssetWrapper,
+ Asset,
+ Audio,
+ Checkbox,
+ EOI,
+ File,
+ GoogleDrive,
+ GracefulImage,
+ LazyImage,
+ LiteYouTubeEmbed,
+ PageIcon,
+ PageTitle,
+ SyncPointerBlock,
+ Text
+} from '../packages/nreact/src'
+import type { Block as BlockType } from '../packages/ntypes/src/block'
+
+const emptyRecordMap = {
+ block: {},
+ collection: {},
+ collection_view: {},
+ notion_user: {},
+ collection_query: {},
+ signed_urls: {}
+}
+
+const baseBlock: BlockType = {
+ id: 'page',
+ type: 'page',
+ properties: { title: [['Test']] },
+ parent_id: '',
+ parent_table: 'space',
+ version: 1,
+ created_time: 0,
+ last_edited_time: 0,
+ alive: true,
+ created_by_table: 'user',
+ created_by_id: '',
+ last_edited_by_table: 'user',
+ last_edited_by_id: ''
+} as any
+
+const audioBlock: BlockType = {
+ ...baseBlock,
+ id: 'audio',
+ type: 'audio',
+ properties: { source: [['https://example.com/audio.mp3']] }
+} as any
+
+const fileBlock: BlockType = {
+ ...baseBlock,
+ id: 'file',
+ type: 'file',
+ properties: {
+ title: [['File']],
+ size: [['1']],
+ source: [['https://example.com/file.pdf']]
+ }
+} as any
+
+const imageBlock: BlockType = {
+ ...baseBlock,
+ id: 'image',
+ type: 'image',
+ properties: {
+ source: [['https://example.com/image.png']]
+ }
+} as any
+
+const googleDriveBlock: BlockType = {
+ ...baseBlock,
+ id: 'gd',
+ type: 'google_drive',
+ format: { drive_properties: { url: 'https://drive.google.com/file/d/abc' } }
+} as any
+
+const eoiBlock: BlockType = {
+ ...baseBlock,
+ id: 'eoi',
+ type: 'embed',
+ format: {
+ original_url: 'https://github.com',
+ domain: 'github.com',
+ attributes: [{ id: 'title', values: ['Repo'] }]
+ }
+} as any
+
+function renderElement(element: React.ReactNode, expectContent = true) {
+ const html = renderToString(
+ {element}
+ )
+ // Components that render nothing (return null) will produce empty string
+ // This is valid behavior, we just want to ensure no errors during rendering
+ if (expectContent) expect(html.length).toBeGreaterThan(0)
+}
+
+describe('individual component rendering', () => {
+ test('Block', () => {
+ renderElement()
+ })
+
+ test('AssetWrapper', () => {
+ renderElement()
+ })
+
+ test('Asset', () => {
+ renderElement({null})
+ })
+
+ test('Audio', () => {
+ renderElement()
+ })
+
+ test('Checkbox', () => {
+ renderElement()
+ })
+
+ test('EOI', () => {
+ renderElement()
+ })
+
+ test('File', () => {
+ renderElement()
+ })
+
+ test('GoogleDrive', () => {
+ renderElement()
+ })
+
+ test('GracefulImage', () => {
+ renderElement()
+ })
+
+ test('LazyImage', () => {
+ renderElement()
+ })
+
+ test('LiteYouTubeEmbed', () => {
+ renderElement()
+ })
+
+ test('PageIcon', () => {
+ renderElement()
+ })
+
+ test('PageTitle', () => {
+ renderElement()
+ })
+
+ test('SyncPointerBlock', () => {
+ // SyncPointerBlock returns null when there's no transclusion_reference_pointer
+ renderElement(, false)
+ })
+
+ test('Text', () => {
+ renderElement()
+ })
+})
diff --git a/test/rsc-render.test.tsx b/test/rsc-render.test.tsx
new file mode 100644
index 00000000..d3978631
--- /dev/null
+++ b/test/rsc-render.test.tsx
@@ -0,0 +1,40 @@
+import { test, expect } from 'vitest'
+import React from '../packages/nreact/node_modules/react'
+import { renderToString } from '../packages/nreact/node_modules/react-dom/server'
+import { NotionRenderer } from '../packages/nreact/src/renderer'
+import type { ExtendedRecordMap } from '../packages/ntypes/src/maps'
+
+const recordMap: ExtendedRecordMap = {
+ block: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ '1': {
+ role: 'reader',
+ value: {
+ id: '1',
+ type: 'page',
+ properties: { title: [['Test']] },
+ parent_id: '',
+ parent_table: 'space',
+ version: 1,
+ created_time: 0,
+ last_edited_time: 0,
+ alive: true,
+ created_by_table: 'user',
+ created_by_id: '',
+ last_edited_by_table: 'user',
+ last_edited_by_id: ''
+ }
+ }
+ },
+ collection: {},
+ collection_view: {},
+ notion_user: {},
+ collection_query: {},
+ signed_urls: {}
+}
+
+test('server component renders to string', async () => {
+ const html = renderToString()
+ expect(html).toBeTruthy()
+ expect(html.length).toBeGreaterThan(0)
+})