Files
020d79bb27 [examples] migrate cloudinary to vercel-blob (#93762)
## Summary

Replaces the Cloudinary-backed image gallery example with a Vercel
Blob–backed equivalent. The migration is driven by the broader move of
[`vercel/nextconf-image-gallery`](https://github.com/vercel/nextconf-image-gallery)
(the deployed companion to this example) off Cloudinary.

- Renamed `examples/with-cloudinary` → `examples/with-vercel-blob` via
`git mv` (history preserved).
- Images are discovered at build time via the `@vercel/blob` SDK's
`list()` API, then probed with
[`sharp`](https://github.com/lovell/sharp) for dimensions and to
pre-generate base64 blur placeholders. `next/image` (Vercel's image
optimizer) handles resizing — no Cloudinary `c_scale,w_*` transform
URLs.
- `ImageProps` is now `{ id, url, width, height, blurDataUrl }` (was `{
public_id, format, ... }`).
- `utils/cachedImages.ts` does the listing + dimension probe + blur
placeholder generation.
- Removed `utils/cloudinary.ts` and `utils/generateBlurPlaceholder.ts`.
- `next.config.js` `remotePatterns` now allows
`*.public.blob.vercel-storage.com`.
- Deps: dropped `cloudinary`, `imagemin`, `imagemin-jpegtran`; added
`@vercel/blob` and `sharp`.
- `.env.local.example` reduced to a single `BLOB_READ_WRITE_TOKEN`.
- README rewritten: blob description, deploy-button env, setup steps,
references.

## Test plan

- [x] `npx create-next-app --example with-vercel-blob ./blob-app` clones
cleanly
- [x] After populating a blob store and `.env.local`, `npm run dev`
renders the gallery with blur placeholders
- [x] `npm run build` succeeds (sharp probe + placeholder generation
runs at build time)
- [x] Vercel deploy button flow prompts only for `BLOB_READ_WRITE_TOKEN`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Co-authored-by: Joseph <[email protected]>
2026-05-12 11:01:10 +02:00

146 lines
5.4 KiB
TypeScript

import type { NextPage } from "next";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef } from "react";
import Bridge from "../components/Icons/Bridge";
import Logo from "../components/Icons/Logo";
import Modal from "../components/Modal";
import getResults from "../utils/cachedImages";
import type { ImageProps } from "../utils/types";
import { useLastViewedPhoto } from "../utils/useLastViewedPhoto";
const Home: NextPage = ({ images }: { images: ImageProps[] }) => {
const router = useRouter();
const { photoId } = router.query;
const [lastViewedPhoto, setLastViewedPhoto] = useLastViewedPhoto();
const lastViewedPhotoRef = useRef<HTMLAnchorElement>(null);
useEffect(() => {
// This effect keeps track of the last viewed photo in the modal to keep the index page in sync when the user navigates back
if (lastViewedPhoto && !photoId) {
lastViewedPhotoRef.current.scrollIntoView({ block: "center" });
setLastViewedPhoto(null);
}
}, [photoId, lastViewedPhoto, setLastViewedPhoto]);
return (
<>
<Head>
<title>Next.js Conf 2022 Photos</title>
<meta
property="og:image"
content="https://nextjsconf-pics.vercel.app/og-image.png"
/>
<meta
name="twitter:image"
content="https://nextjsconf-pics.vercel.app/og-image.png"
/>
</Head>
<main className="mx-auto max-w-[1960px] p-4">
{photoId && (
<Modal
images={images}
onClose={() => {
setLastViewedPhoto(photoId);
}}
/>
)}
<div className="columns-1 gap-4 sm:columns-2 xl:columns-3 2xl:columns-4">
<div className="after:content relative mb-5 flex h-[629px] flex-col items-center justify-end gap-4 overflow-hidden rounded-lg bg-white/10 px-6 pb-16 pt-64 text-center text-white shadow-highlight after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:shadow-highlight lg:pt-0">
<div className="absolute inset-0 flex items-center justify-center opacity-20">
<span className="flex max-h-full max-w-full items-center justify-center">
<Bridge />
</span>
<span className="absolute left-0 right-0 bottom-0 h-[400px] bg-gradient-to-b from-black/0 via-black to-black"></span>
</div>
<Logo />
<h1 className="mt-8 mb-4 text-base font-bold uppercase tracking-widest">
2022 Event Photos
</h1>
<p className="max-w-[40ch] text-white/75 sm:max-w-[32ch]">
Our incredible Next.js community got together in San Francisco for
our first ever in-person conference!
</p>
<a
className="pointer z-10 mt-6 rounded-lg border border-white bg-white px-3 py-2 text-sm font-semibold text-black transition hover:bg-white/10 hover:text-white md:mt-4"
href="https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-vercel-blob&project-name=nextjs-image-gallery&repository-name=with-vercel-blob&env=BLOB_READ_WRITE_TOKEN&envDescription=Read%2Fwrite%20token%20for%20your%20Vercel%20Blob%20store"
target="_blank"
rel="noreferrer"
>
Clone and Deploy
</a>
</div>
{images.map(({ id, url, blurDataUrl }) => (
<Link
key={id}
href={`/?photoId=${id}`}
as={`/p/${id}`}
ref={id === Number(lastViewedPhoto) ? lastViewedPhotoRef : null}
shallow
className="after:content group relative mb-5 block w-full cursor-zoom-in after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:shadow-highlight"
>
<Image
alt="Next.js Conf photo"
className="transform rounded-lg brightness-90 transition will-change-auto group-hover:brightness-110"
style={{ transform: "translate3d(0, 0, 0)" }}
placeholder="blur"
blurDataURL={blurDataUrl}
src={url}
width={720}
height={480}
sizes="(max-width: 640px) 100vw,
(max-width: 1280px) 50vw,
(max-width: 1536px) 33vw,
25vw"
/>
</Link>
))}
</div>
</main>
<footer className="p-6 text-center text-white/80 sm:p-12">
Thank you to{" "}
<a
href="https://edelsonphotography.com/"
target="_blank"
className="font-semibold hover:text-white"
rel="noreferrer"
>
Josh Edelson
</a>
,{" "}
<a
href="https://www.newrevmedia.com/"
target="_blank"
className="font-semibold hover:text-white"
rel="noreferrer"
>
Jenny Morgan
</a>
, and{" "}
<a
href="https://www.garysextonphotography.com/"
target="_blank"
className="font-semibold hover:text-white"
rel="noreferrer"
>
Gary Sexton
</a>{" "}
for the pictures.
</footer>
</>
);
};
export default Home;
export async function getStaticProps() {
return {
props: {
images: await getResults(),
},
};
}