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/cli/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default defineConfig({
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
'child_process',
'fs',
'fs/promises',
'path',
Expand All @@ -27,4 +28,3 @@ export default defineConfig({
},
plugins: [dts({ tsconfigPath: 'tsconfig.json', outDir: 'build/src' })]
})

13 changes: 12 additions & 1 deletion packages/nreact/src/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,14 @@ export const Block: React.FC<BlockProps> = props => {
<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} />
<LazyImage
src={mapImageUrl(block.format?.bookmark_icon, block)}
alt={title}
onError={e => {
const target = e.currentTarget as HTMLImageElement
target.style.display = 'none'
}}
/>
</div>
)}

Expand All @@ -558,6 +565,10 @@ export const Block: React.FC<BlockProps> = props => {
style={{
objectFit: 'cover'
}}
onError={e => {
const parent = (e.currentTarget as HTMLImageElement).closest('.notion-bookmark-image')
if (parent instanceof HTMLElement) parent.style.display = 'none'
}}
/>
</div>
)}
Expand Down
4 changes: 3 additions & 1 deletion packages/nreact/src/components/lazy-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const LazyImage: React.FC<{
height?: number
zoomable?: boolean
priority?: boolean
}> = ({ src, alt, className, style, zoomable = false, priority = false, height, ...rest }) => {
onError?: React.ReactEventHandler<HTMLImageElement>
}> = ({ src, alt, className, style, zoomable = false, priority = false, height, onError, ...rest }) => {
const { recordMap, zoom, previewImages, forceCustomImages, components } = useNotionContext()

const zoomRef = React.useRef(zoom ? zoom.clone() : null)
Expand Down Expand Up @@ -95,6 +96,7 @@ export const LazyImage: React.FC<{
ref={attachZoomRef}
loading='lazy'
decoding='async'
onError={onError}
{...rest}
/>
)
Expand Down
4 changes: 3 additions & 1 deletion packages/nutils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"pu": "pnpm publish"
},
"devDependencies": {
"vite-plugin-dts": "^3.9.1"

"vite-plugin-dts": "^4.5.4"

},
"dependencies": {
"is-url-superb": "^6.1.0",
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 @@ -19,6 +19,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
Loading