Skip to content
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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Conversation

barbaraperic
Copy link
Collaborator

@barbaraperic barbaraperic commented Dec 16, 2024

📝 Description

Type: Refactor

This PR introduces SmartImage wrapper component (name inspired by SmartTextLink). 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 the image as a prop.

SmartImage will render the fallback (currently fixed but can be customizable in the future) in the following cases:

  • The image is missing in the markdown frontmatter.
  • The src URL in the markdown frontmatter is invalid.
  • A remote image URL is passed, but the URL is broken.

🧪 How to Test

  • Check if the fallback is shown in the cases described above.

@barbaraperic barbaraperic self-assigned this Dec 16, 2024
Copy link

[FF] Validate Image Src

Copy link

vercel bot commented Dec 16, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
filecoin-foundation-site ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 20, 2024 9:06am

{...commonProps}
className={clsx(commonProps.className, 'aspect-video')}
src={image.data}
src={(image.data as StaticImageData).src}
Copy link
Collaborator Author

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)

Copy link
Collaborator

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

Copy link
Collaborator Author

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'
Copy link
Collaborator Author

@barbaraperic barbaraperic Dec 16, 2024

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

Copy link
Collaborator

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?

Copy link
Collaborator Author

@barbaraperic barbaraperic Dec 19, 2024

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.

@barbaraperic barbaraperic marked this pull request as ready for review December 17, 2024 13:38
@mirhamasala mirhamasala requested review from CharlyMartin and removed request for mirhamasala December 17, 2024 14:11
Copy link
Collaborator

@CharlyMartin CharlyMartin left a 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'
Copy link
Collaborator

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?

Comment on lines +78 to +86
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
}
}
Copy link
Collaborator

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?

Suggested change
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()
}

Copy link
Collaborator Author

@barbaraperic barbaraperic Dec 19, 2024

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
Copy link
Collaborator

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'>

Copy link
Collaborator Author

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?

Comment on lines 6 to 7
import { type ImageProps } from 'next/image'
import Image from 'next/image'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Group imports?


export async function SmartImage({
src,
alt = '',
Copy link
Collaborator

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does padding do?

Copy link
Collaborator Author

@barbaraperic barbaraperic Dec 19, 2024

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

padding?: boolean
} & Omit<ImageProps, 'src' | 'alt' | 'className'>

export async function SmartImage({
Copy link
Collaborator

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)
}

Copy link
Collaborator Author

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.

{...commonProps}
className={clsx(commonProps.className, 'aspect-video')}
src={image.data}
src={(image.data as StaticImageData).src}
Copy link
Collaborator

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

@barbaraperic
Copy link
Collaborator Author

PS: Should statically imported images be part of the smart image component? Since we already know these are valid, it might not be necessary.

Yes, good observation!

- Simplify SmartImage component and improve type safety
- Remove redundant static image handling logic
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants