Skip to content
Open
Changes from 1 commit
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
Next Next commit
fix: improve image block link handling logic and add URL validation
  • Loading branch information
raymondanythings committed Jun 1, 2025
commit 3f760f17b9ede2e5cc4a31e1b5ed032321b5e29e
68 changes: 44 additions & 24 deletions packages/react-notion-x/src/components/asset-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type * as React from 'react'
import { type BaseContentBlock, type Block } from 'notion-types'
import {
type BaseContentBlock,
type Block,
type ImageBlock
} from 'notion-types'
import { parsePageId } from 'notion-utils'

import { useNotionContext } from '..'
Expand All @@ -19,18 +23,11 @@ export function AssetWrapper({
const value = block as BaseContentBlock
const { components, mapPageUrl, rootDomain, zoom } = useNotionContext()

let isURL = false
if (block.type === 'image') {
const caption: string | undefined = value?.properties?.caption?.[0]?.[0]
if (caption) {
const id = parsePageId(caption, { uuid: true })
const caption = value.properties?.caption?.[0]?.[0]
const imageHyperlink = (value as ImageBlock).format?.image_hyperlink

const isPage = caption.charAt(0) === '/' && id
if (isPage || isValidURL(caption)) {
isURL = true
}
}
}
const availableLinks = [imageHyperlink, caption].filter(Boolean) as string[]
const urlInfo = getURL(value, availableLinks)

const figure = (
<figure
Expand All @@ -41,8 +38,8 @@ export function AssetWrapper({
blockId
)}
>
<Asset block={value} zoomable={zoom && !isURL}>
{value?.properties?.caption && !isURL && (
<Asset block={value} zoomable={zoom && !urlInfo}>
{value?.properties?.caption && (
<figcaption className='notion-asset-caption'>
<Text value={value.properties.caption} block={block} />
</figcaption>
Expand All @@ -52,20 +49,14 @@ export function AssetWrapper({
)

// allows for an image to be a link
if (isURL) {
const caption: string | undefined = value?.properties?.caption?.[0]?.[0]
const id = parsePageId(caption, { uuid: true })
const isPage = caption?.charAt(0) === '/' && id
const captionHostname = extractHostname(caption)

if (urlInfo?.url) {
const urlHostName = extractHostname(urlInfo.url)
return (
<components.PageLink
style={urlStyle}
href={isPage ? mapPageUrl(id) : caption}
href={urlInfo.type === 'page' ? mapPageUrl(urlInfo.url) : urlInfo.url}
target={
captionHostname &&
captionHostname !== rootDomain &&
!caption?.startsWith('/')
urlHostName && urlHostName !== rootDomain && !caption?.startsWith('/')
? 'blank_'
: null
}
Expand All @@ -78,6 +69,35 @@ export function AssetWrapper({
return figure
}

function getURL(
block: BaseContentBlock,
availableLinks: string[]
): {
id?: string
type: 'page' | 'external'
url: string
} | null {
if (block.type !== 'image') {
return null
}

for (const link of availableLinks) {
if (!link) continue

const id = parsePageId(link, { uuid: true })
const isPage = link.charAt(0) === '/' && id

if (isPage || isValidURL(link)) {
return {
id: id ?? undefined,
type: isValidURL(link) ? 'external' : 'page',
url: link
}
}
}
return null
}

function isValidURL(str: string) {
// TODO: replace this with a more well-tested package
const pattern = new RegExp(
Expand Down