-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[UXIT-1731] Validate Image Src #919
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
src/app/_components/Card.tsx
Outdated
{...commonProps} | ||
className={clsx(commonProps.className, 'aspect-video')} | ||
src={image.data} | ||
src={(image.data as StaticImageData).src} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typescript is complaining here although we have a isStaticImage
condition (which check data
in the image
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should just be image.src
no? image.data
doesn't exist on SmartImageProps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry not sure I understand 😕 StaticImages
have data key, like this:
{
data: {
src: '/_next/static/media/022624-ff-anualreport.b5deae8f.png',
height: 1536,
width: 3072,
blurDataURL: '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F022624-ff-anualreport.b5deae8f.png&w=8&q=70',
blurWidth: 8,
blurHeight: 4
},
alt: '',
sizes: '(min-width: 640px) 710px, (min-width: 768px) 980px, (min-width: 1024px) 480px, 100vw'
}
Ah so this should actually be image.data
...
@@ -0,0 +1,86 @@ | |||
'use server' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need to be explicit to run on the server because fs
and path
exclusively run in the node env
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't components 'use server'
by default?
I haven't seen this used in components. What does it do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically makes sure it runs server side. For example, in GovernanceCalendarCard
(use client
component) we import Card
component which uses SmartImage
. if we don't explicitly write use server
in the SmartImage
component, it will run on the client, and in that case we get an error because we use fs
which can't run in the browser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a first pass with quite a few questions / suggestions.
Happy to have another look once the comments are resolved.
Thanks Barbara!
PS: Should statically imported images be part of the smart image component? Since we already know these are valid, it might not be necessary.
@@ -0,0 +1,86 @@ | |||
'use server' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't components 'use server'
by default?
I haven't seen this used in components. What does it do?
async function checkAssetImageExists(src: string) { | ||
const publicPath = path.join(process.cwd(), 'public', src) | ||
try { | ||
await fs.access(publicPath, fs.constants.F_OK) | ||
return true | ||
} catch { | ||
return false | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reads a bit better maybe?
async function checkAssetImageExists(src: string) { | |
const publicPath = path.join(process.cwd(), 'public', src) | |
try { | |
await fs.access(publicPath, fs.constants.F_OK) | |
return true | |
} catch { | |
return false | |
} | |
} | |
async function checkAssetImageExists(src: string) { | |
const publicPath = path.join(process.cwd(), 'public', src) | |
const stats = await fs.stat(publicPath) | |
return stats.isFile() | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, I like how async await reads, but this made me think we could use try catch to capture errors? what do you think?
async function checkAssetImageExists(src: string) {
const publicPath = path.join(process.cwd(), 'public', src);
try {
await fs.access(publicPath, fs.constants.F_OK);
return true;
} catch (error) {
console.error(`Error checking asset image existence at path "${publicPath}":`, error);
return false;
}
}
export type SmartImageProps = { | ||
src?: string | StaticImageProps['data'] | ||
alt?: string | ||
className?: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
className?: string
can be included in Omit<ImageProps, 'src' | 'alt'>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm it's included in the line 21? Or you're referring to something else?
src/app/_components/SmartImage.tsx
Outdated
import { type ImageProps } from 'next/image' | ||
import Image from 'next/image' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Group imports?
src/app/_components/SmartImage.tsx
Outdated
|
||
export async function SmartImage({ | ||
src, | ||
alt = '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we make alt
an empty string if undefined
? I don't understand 🙃
alt?: string | ||
className?: string | ||
objectFit?: ImageObjectFit | ||
padding?: boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does padding
do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we use it in the Card component, same as objectFit
src/app/_components/SmartImage.tsx
Outdated
padding?: boolean | ||
} & Omit<ImageProps, 'src' | 'alt' | 'className'> | ||
|
||
export async function SmartImage({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about something like this?
Does it read a bit better to you?
export async function SmartImage({ src, alt = '', ...props }: SmartImageProps) {
if (!src) {
return <Image src={fallbackSrc} alt={fallbackAlt} {...props} />
}
const isValid = await checkImageValidity(src)
return (
<Image
src={isValid ? src : fallbackSrc}
alt={isValid ? alt : fallbackAlt}
{...props}
/>
)
}
async function checkImageValidity(src: NonNullable<SmartImageProps['src']>) {
const isStaticImport = typeof src === 'object'
const isAssetImage = typeof src === 'string' && src.startsWith('/assets')
if (isStaticImport) {
return true
}
if (isAssetImage) {
return await checkAssetImageExists(src)
}
return await checkRemoteImageExists(src)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean, personally I kind of like the separation of logic (getImageSrc
) and ui rendering (SmartImage
). That said, I’m happy to explore versions if you think this doesn’t work well for the current context.
src/app/_components/Card.tsx
Outdated
{...commonProps} | ||
className={clsx(commonProps.className, 'aspect-video')} | ||
src={image.data} | ||
src={(image.data as StaticImageData).src} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should just be image.src
no? image.data
doesn't exist on SmartImageProps
Yes, good observation! |
- Simplify SmartImage component and improve type safety - Remove redundant static image handling logic
📝 Description
Type: Refactor
This PR introduces
SmartImage
wrapper component (name inspired bySmartTextLink
). The purpose of the component is to handle image URL validity and conditionally render the fallback.🛠️ Key Changes
Currently it is used in
Card
,PageHeader
(app/components) ,ArticleHeader
,PostHeader
(ecosystem_explorer/components). These components require fallback handling because images in markdown frontmatter are optional.With use of this component we can remove the "
image || graphicsData.imageFallback.data
" condition and just pass theimage
as a prop.SmartImage
will render the fallback (currently fixed but can be customizable in the future) in the following cases:🧪 How to Test