Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/nclient/src/notion-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,5 @@ test(`Get block`, { timeout: 10000, concurrent: true }, async () => {
const id = '3f9e0d86-c643-4672-aa0c-78d63fa80598'
const api = new NotionAPI()
const res = await api.getBlocks([id])
expect(res.recordMap.block[id].role).toBe('none')
expect(res.recordMap).toBeTruthy()
})
96 changes: 49 additions & 47 deletions packages/nreact/src/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -518,60 +518,62 @@ export const Block: React.FC<BlockProps> = props => {
}

return (
<a
target='_blank'
rel='noopener noreferrer'
className={`notion-bookmark ${block.format?.block_color ? `notion-${block.format.block_color}` : ''} ${blockId}`}
href={link[0][0]}>
<div>
{title && (
<div className='notion-bookmark-title'>
<Text value={[[title]]} block={block} />
</div>
)}

{block.properties.description && (
<div className='notion-bookmark-description'>
<Text value={block.properties.description} block={block} />
</div>
)}
<div className='notion-row'>
<components.Link
target='_blank'
rel='noopener noreferrer'
className={`notion-bookmark ${block.format?.block_color ? `notion-${block.format.block_color}` : ''} ${blockId}`}
href={link[0][0]}>
<div>
{title && (
<div className='notion-bookmark-title'>
<Text value={[[title]]} block={block} />
</div>
)}

<div className='notion-bookmark-link'>
{block.format?.bookmark_icon && (
<div className='notion-bookmark-link-icon'>
<LazyImage
src={mapImageUrl(block.format.bookmark_icon, block)}
alt={title}
onError={e => {
const parent = e.currentTarget.closest('.notion-bookmark-link-icon') as HTMLElement
if (parent) parent.style.display = 'none'
}}
/>
{block.properties?.description && (
<div className='notion-bookmark-description'>
<Text value={block.properties?.description} block={block} />
</div>
)}

<div className='notion-bookmark-link-text'>
<Text value={link} block={block} />
<div className='notion-bookmark-link'>
{block.format?.bookmark_icon && (
<div className='notion-bookmark-link-icon'>
<LazyImage
src={mapImageUrl(block.format?.bookmark_icon, block)}
alt={title}
onError={e => {
const parent = e.currentTarget.closest('.notion-bookmark-link-icon') as HTMLElement
if (parent) parent.style.display = 'none'
}}
/>
</div>
)}

<div className='notion-bookmark-link-text'>
<Text value={link} block={block} />
</div>
</div>
</div>
</div>

{block.format?.bookmark_cover && (
<div className='notion-bookmark-image'>
<LazyImage
src={mapImageUrl(block.format.bookmark_cover, block)}
alt={getTextContent(block.properties.title)}
style={{
objectFit: 'cover'
}}
onError={e => {
const parent = e.currentTarget.closest('.notion-bookmark-image') as HTMLElement
if (parent) parent.style.display = 'none'
}}
/>
</div>
)}
</a>
{block.format?.bookmark_cover && (
<div className='notion-bookmark-image'>
<LazyImage
src={mapImageUrl(block.format?.bookmark_cover, block)}
alt={getTextContent(block.properties?.title)}
style={{
objectFit: 'cover'
}}
onError={e => {
const parent = e.currentTarget.closest('.notion-bookmark-image') as HTMLElement
if (parent) parent.style.display = 'none'
}}
/>
</div>
)}
</components.Link>
</div>
)
}

Expand Down
63 changes: 63 additions & 0 deletions packages/nutils/src/map-image-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { test, expect } from 'vitest'
import { defaultMapImageUrl } from './map-image-url'
import type { Block } from '@texonom/ntypes'

const mockBlock: Block = {
id: 'test-block-id',
parent_table: 'block',
parent_id: 'test-parent-id',
type: 'bookmark',
version: 1,
alive: true,
created_time: 0,
last_edited_time: 0,
created_by_table: 'user',
created_by_id: '',
last_edited_by_table: 'user',
last_edited_by_id: ''
} as Block

test('returns null for empty url', () => {
expect(defaultMapImageUrl('', mockBlock)).toBe(null)
})

test('returns data URLs as-is', () => {
expect(defaultMapImageUrl('data:image/png;base64,abc', mockBlock)).toBe('data:image/png;base64,abc')
})

test('returns unsplash URLs as-is', () => {
expect(defaultMapImageUrl('https://images.unsplash.com/photo-123', mockBlock)).toBe(
'https://images.unsplash.com/photo-123'
)
})

test('proxies notion-static URLs through notion.so', () => {
const url = 'https://www.notion.so/image/test.jpg'
const result = defaultMapImageUrl(url, mockBlock)
expect(result).toContain('notion.so')
})

test('returns external HTTPS URLs as-is (no proxy)', () => {
const externalUrls = [
'https://opengraph.githubassets.com/abc/repo',
'https://cdn.example.com/image.jpg',
'https://roadmap.sh/og-image.png',
'https://velog.velcdn.com/images/test.jpg',
'https://developer.mozilla.org/favicon.ico'
]
for (const url of externalUrls) {
const result = defaultMapImageUrl(url, mockBlock)
expect(result).toBe(url)
}
})

test('still proxies notion.so relative paths', () => {
const result = defaultMapImageUrl('/images/page-cover/test.jpg', mockBlock)
expect(result).toContain('notion.so')
})

test('still proxies S3 notion-static URLs without signatures', () => {
const url = 'https://s3.us-west-2.amazonaws.com/secure.notion-static.com/image.jpg'
const result = defaultMapImageUrl(url, mockBlock)
expect(result).toContain('notion.so')
})
3 changes: 3 additions & 0 deletions packages/nutils/src/map-image-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const defaultMapImageUrl = (url: string, block: Block): string | null =>
)
// if the URL is already signed, then use it as-is
return url

// external HTTPS URLs that aren't from notion.so or amazonaws should bypass the proxy
if (u.protocol === 'https:' && !u.hostname.endsWith('notion.so') && !u.hostname.endsWith('amazonaws.com')) return url
} catch {
// ignore invalid urls
}
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"listFiles": false,
"pretty": true,
"lib": ["ESNext", "ESNext.Array", "DOM"],
"baseUrl": "."
"types": ["node"],
"baseUrl": ".",
"ignoreDeprecations": "6.0"
}
}
2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"tasks": {
"build": {},
"build": { "outputs": ["build/**"] },
"@texonom/nclient#build": { "dependsOn": ["@texonom/ntypes#build", "@texonom/nutils#build"] },
"@texonom/ncompat#build": { "dependsOn": ["@texonom/ntypes#build", "@texonom/nutils#build"] },
"@texonom/nutils#build": { "dependsOn": ["@texonom/ntypes#build"] },
Expand Down