{"id":33603,"date":"2024-04-30T07:00:00","date_gmt":"2024-04-30T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=33603"},"modified":"2025-04-16T13:14:55","modified_gmt":"2025-04-16T20:14:55","slug":"image-gallery-next-js-parallel-intercepting-routes","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes","title":{"rendered":"Build an Image Gallery: Next.js Parallel and Intercepting Routes"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Designing a website entails overseeing an extensive collection of visuals. Managing this diverse range of images while keeping them current presents a significant challenge for development teams.<\/p>\n<p>In this blog post, we\u2019ll address a typical UI pattern where users can interact with images in a gallery through client-side navigation or access them directly via shareable URLs. By leveraging Cloudinary Digital Asset Management solutions together with the latest features of Next.js (parallel and intercepting routes), we\u2019ll explore how these functionalities can be combined to address typical challenges encountered when implementing modal interfaces, including:<\/p>\n<p>Enabling modal content to be shareable via URL links.\nMaintaining context when the page is refreshed rather than closing the modal.\nImplement modal closure during backward navigation instead of returning to the previous route.\nFacilitating modal reopening during forward navigation.<\/p>\n<h2>Next.js App Router<\/h2>\n<p>In version 13, Next.js introduced a new App Router built on <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/rendering\/server-components\">React Server Components<\/a>. This router supports shared layouts, nested routing, loading states, error handling, and more.<\/p>\n<p>The App Router works in a new directory named <code>app<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-1.png\" alt=\"next.js app router folders structure for main page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"401\" height=\"76\"\/><\/p>\n<p>Creating a <code>page.tsx<\/code> file inside the <code>app\/main-page<\/code> folder allows you to define what users will see when they navigate to <code>\/main-page<\/code>. So folder names inside the app directory define your app routes.<\/p>\n<p>Creating nested routes is simple, you just have to follow this folder pattern:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-2.png\" alt=\"next.js app router folders structure for nested route\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"326\" height=\"129\"\/><\/p>\n<p>In this case, the URL of your page will be <code>\/main-page\/nested-page<\/code>.<\/p>\n<h2>Exploring Next.js Parallel and Intercepting Routes<\/h2>\n<p><strong>Parallel Routes<\/strong> in Next.js allow for the simultaneous rendering of multiple pages or sections within the same layout. They are particularly useful for dynamic areas of an application, such as dashboards or social media feeds.<\/p>\n<p>You use named slots defined with the @folder convention to create parallel routes. For instance, if you have slots for left and right page sections, you\u2019d define them as @left and @right.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-3.png\" alt=\"next.js parallel routes folders structure left and right section in the main page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"465\" height=\"346\"\/><\/p>\n<p>These slots are then passed as props to the shared parent layout component, which can render them alongside the main content.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-4.png\" alt=\"next.js parallel routes layout file code\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"671\" height=\"599\"\/><\/p>\n<p>It\u2019s important to note that slots don\u2019t affect the URL structure. Even if you have a slot like @left, the URL won\u2019t change to include that slot name.<\/p>\n<p>By default, Next.js manages each slot\u2019s active state. During client-side navigation, it updates the content within a slot without affecting the other slots. However, Next.js may render a default component for unmatched slots during a full-page reload or display a 404 error if no default component is defined.<\/p>\n<p>You can define a default component as a fallback to handle unmatched slots during initial loads or full-page reloads. This ensures a consistent user experience even when navigating between different application sections.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-5.png\" alt=\"next.js parallel routes default file definition\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"465\" height=\"346\"\/><\/p>\n<p><strong>Intercepting routes<\/strong> in Next.js enables loading a route from another part of the application within the current layout. This approach proves beneficial when you aim to seamlessly display route content without disrupting the user\u2019s context.<\/p>\n<p>For instance, suppose you click a photo in a feed. Instead of navigating to a separate page, the photo appears in a modal overlaying the feed. Next.js intercepts the photo route, conceals the URL, and presents the content within the existing feed layout.<\/p>\n<p>However, when accessing the photo via a shareable URL or refreshing the page, the entire photo page should render independently, bypassing route interception.<\/p>\n<p>Intercepting routes utilize the (..) convention. This convention allows you to match segments at different levels:<\/p>\n<ul>\n<li>(.) matches segments on the same level.<\/li>\n<li>(..) matches segments one level above.<\/li>\n<li>(..)(..) matches segments two levels above.<\/li>\n<li>(\u2026) matches segments from the root app directory.<\/li>\n<\/ul>\n<h2>Let\u2019s Start Building<\/h2>\n<p>Combining these two new Next.js concepts with Cloudinary\u2019s Digital Asset Management solutions, we will build an image gallery where each image is clickable. Clicking on one of the images will open a modal with a preview of the image itself, and the user will also be able to navigate to the image page directly using a shareable URL.<\/p>\n<h3>Setting Up a Cloudinary Account<\/h3>\n<p>After creating an account and signing up for free with Cloudinary, you\u2019ll have access to your dashboard, where all the API keys you\u2019ll need to build this example will be available.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-6.png\" alt=\"cloudinary-api-keys\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1999\" height=\"261\"\/><\/p>\n<p>Additionally, you can start uploading all the assets you want directly through your Cloudinary console. For simplicity, in this example, we\u2019ll use the images that you\u2019ll find already uploaded by default.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-7.png\" alt=\"cloudinary-uploaded-images-examples\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1999\" height=\"508\"\/><\/p>\n<h3>Our Next.js App<\/h3>\n<p>Let\u2019s initialize now our Next.js App with the command:<\/p>\n<p><code>npx create-next-app@latest<\/code><\/p>\n<p>Our folder structure will be as follows:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-8.png\" alt=\"next.js image gallery example folders structure with all the files\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"625\" height=\"462\"\/><\/p>\n<p>We\u2019ll create a Parallel Route for our Modal <code>@modal<\/code> and intercepting the <code>photo<\/code> route with an Intercepting Route. Since <code>@modal<\/code> is a Slot and not a segment, the route we\u2019d like to intercept is at the same level as our modal, so we must use the <code>(.)<\/code> syntax.<\/p>\n<p>Our <code>layout.tsx<\/code> file will have the <code>modal<\/code> slot, as you can see, together with the <code>children<\/code>, which is going to be our <code>page.tsx<\/code> file with the image gallery.<\/p>\n<p><code>app\/layout.tsx:<\/code><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">RootLayout<\/span>(<span class=\"hljs-params\">{\n  children,\n  modal\n}: Readonly&lt;{\n  children: React.ReactNode;\n  modal: React.ReactNode\n}&gt;<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">html<\/span> <span class=\"hljs-attr\">lang<\/span>=<span class=\"hljs-string\">\"en\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">body<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">{inter.className}<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"grid grid-rows-&#91;auto_1fr_auto] h-screen\"<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n            {children}\n            {modal}\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"modal-root\"<\/span> \/&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">body<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">html<\/span>&gt;<\/span><\/span>\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><code>app\/page.tsx:<\/code><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Home<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Wrapper<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"max-w-7xl\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"sr-only\"<\/span>&gt;<\/span>Image Gallery<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">ul<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"grid grid-cols-2 lg:grid-cols-3 gap-2\"<\/span>&gt;<\/span>\n        {pictures.map(picture =&gt; {\n          return (\n            <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">li<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{picture.id}<\/span>&gt;<\/span>\n              <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Link<\/span>\n                <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"relative group\"<\/span>\n                <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{picture.id}<\/span>\n                <span class=\"hljs-attr\">href<\/span>=<span class=\"hljs-string\">{<\/span>`\/<span class=\"hljs-attr\">photos<\/span>\/${<span class=\"hljs-attr\">picture.id<\/span>}`}\n                <span class=\"hljs-attr\">passHref<\/span>\n              &gt;<\/span>\n                <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldImage<\/span>\n                  <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{picture.image.publicId}<\/span>\n                  <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">{730}<\/span>\n                  <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">{900}<\/span>\n                  <span class=\"hljs-attr\">crop<\/span>=<span class=\"hljs-string\">\"fill\"<\/span>\n                  <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">{picture.image.alt}<\/span>\n                \/&gt;<\/span>\n                <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"lg:opacity-0 lg:group-hover:opacity-100 \n                      transition-opacity absolute bottom-0 left-0\n                      w-full bg-gradient-to-t from-zinc-900 px-5 py-4\"<\/span>&gt;<\/span>\n                  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"text-white text-3xl lg:translate-y-2 \n                        lg:group-hover:translate-y-0 \n                        transition-transform\"<\/span>&gt;<\/span>\n                    {picture.title}\n                  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n                <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n              <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Link<\/span>&gt;<\/span>\n            <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n          )\n        })}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">ul<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Wrapper<\/span>&gt;<\/span><\/span>\n  )\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019ve created the <code>CldImage<\/code> component in our component folder, which is a simple wrapper to the default <code>CdlImage<\/code> component exported from <code>next-cloudinary<\/code> with the <code>'use client'<\/code> directive because Next <a href=\"https:\/\/cloudinary.com\/products\/image\">Cloudinary image<\/a> component is not fully equipped to be shipped in a react server component yet, and since all the page.tsx file in the new Next.js router are server pages by default, we need to be careful and add this directive on top of our image component.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-string\">\"use client\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { CldImage <span class=\"hljs-keyword\">as<\/span> CldImageDefault, CldImageProps } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'next-cloudinary'<\/span>;\n<span class=\"hljs-keyword\">const<\/span> CldImage = <span class=\"hljs-function\">(<span class=\"hljs-params\">props: CldImageProps<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldImageDefault<\/span> {<span class=\"hljs-attr\">...props<\/span>} \/&gt;<\/span><\/span>\n  );\n}\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> CldImage;\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Next, we\u2019ll import in our <code>page.tsx<\/code> file all the <code>pictures<\/code>. This is because we\u2019ve added some more useful data in the images just to enhance our app. Here\u2019s one example of our data structure for images:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json shcb-wrap-lines\">&#91;\n\t{\n\t\t<span class=\"hljs-attr\">\"title\"<\/span>:  <span class=\"hljs-string\">\"Sport\"<\/span>,\n\t\t<span class=\"hljs-attr\">\"id\"<\/span>:  <span class=\"hljs-string\">\"sport\"<\/span>,\n\t\t<span class=\"hljs-attr\">\"image\"<\/span>:  {\n\t\t\t<span class=\"hljs-attr\">\"publicId\"<\/span>:  <span class=\"hljs-string\">\"cld-sample-3\"<\/span>,\n\t\t\t<span class=\"hljs-attr\">\"url\"<\/span>: <span class=\"hljs-string\">\"https:\/\/res.cloudinary.com\/dbyfj6t6c\/image\/upload\/v1711039374\/cld-sample-3.jpg\"<\/span>,\n\t\t\t<span class=\"hljs-attr\">\"width\"<\/span>:  <span class=\"hljs-number\">1870<\/span>, \n\t\t\t<span class=\"hljs-attr\">\"height\"<\/span>:  <span class=\"hljs-number\">1250<\/span>,\n\t\t\t<span class=\"hljs-attr\">\"alt\"<\/span>:  <span class=\"hljs-string\">\"Basketball\"<\/span>\n\t\t}\n\t},\n\t...other images\u2019 data...\n]\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>All this data can be retrieved from our Cloudinary console.<\/p>\n<p>We could also directly fetch all the images we\u2019ve uploaded to our Cloudinary console using the Cloudinary API. We just have to create a fetch function in our <code>page.tsx<\/code> file that fetches all the available images:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getImages<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> url =\n    <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>\/resources\/image`<\/span>\n  <span class=\"hljs-keyword\">const<\/span> auth = Buffer.from(process.env.NEXT_PUBLIC_API_KEY + <span class=\"hljs-string\">':'<\/span>\n    + process.env.NEXT_PUBLIC_API_SECRET).toString(<span class=\"hljs-string\">'base64'<\/span>)\n  <span class=\"hljs-keyword\">const<\/span> res = <span class=\"hljs-keyword\">await<\/span> fetch(url, {\n    <span class=\"hljs-attr\">headers<\/span>: {\n      <span class=\"hljs-attr\">Authorization<\/span>: <span class=\"hljs-string\">`Basic <span class=\"hljs-subst\">${auth}<\/span>`<\/span>\n    }\n  })\n  <span class=\"hljs-keyword\">if<\/span> (!res.ok) {\n    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">'Failed to fetch data'<\/span>)\n  }\n  <span class=\"hljs-keyword\">return<\/span> res.json()\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Once we click one of the images in the gallery, we\u2019ll navigate through the <code>photo\/[id]<\/code> page, but our intercepting route will intercept this path and display the image inside of a modal:<\/p>\n<p><code>app\/@modal\/(.)photos\/modal.tsx:<\/code><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-string\">'use client'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { type ElementRef, useEffect, useRef } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useRouter } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'next\/navigation'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { createPortal } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react-dom'<\/span>;\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Modal<\/span>(<span class=\"hljs-params\">{ children }: { children: React.ReactNode }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> router = useRouter();\n  <span class=\"hljs-keyword\">const<\/span> dialogRef = useRef&lt;ElementRef&lt;<span class=\"hljs-string\">'dialog'<\/span>&gt;&gt;(<span class=\"hljs-literal\">null<\/span>);\n  useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-keyword\">if<\/span> (!dialogRef.current?.open) {\n      dialogRef.current?.showModal();\n    }\n  }, &#91;]);\n  <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">onDismiss<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n    router.back();\n  }\n  <span class=\"hljs-keyword\">return<\/span> createPortal(\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"modal-backdrop\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">dialog<\/span> <span class=\"hljs-attr\">ref<\/span>=<span class=\"hljs-string\">{dialogRef}<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"modal\"<\/span> <span class=\"hljs-attr\">onClose<\/span>=<span class=\"hljs-string\">{onDismiss}<\/span>&gt;<\/span>\n        {children}\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{onDismiss}<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"close-button\"<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">dialog<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>,\n    <span class=\"hljs-built_in\">document<\/span>.getElementById(<span class=\"hljs-string\">'modal-root'<\/span>)!\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><code>app\/@modal\/(.)photos\/page.tsx:<\/code><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { Modal } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/modal'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> CldImage <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/components\/CldImage'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> pictures <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/data\/pictures.json'<\/span>;\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">PhotoModal<\/span>(<span class=\"hljs-params\">{\n  params: { id: photoId },\n}: {\n  params: { id: string };\n}<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> picture = pictures.find(<span class=\"hljs-function\">(<span class=\"hljs-params\">{ id }<\/span>) =&gt;<\/span> id === photoId);\n  <span class=\"hljs-keyword\">if<\/span> (!picture) <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Modal<\/span>&gt;<\/span>No Image found<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Modal<\/span>&gt;<\/span><\/span>\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Modal<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldImage<\/span>\n        <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{picture.image.publicId}<\/span>\n        <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">{730}<\/span>\n        <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">{900}<\/span>\n        <span class=\"hljs-attr\">crop<\/span>=<span class=\"hljs-string\">\"fill\"<\/span>\n        <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">{picture.image.alt}<\/span>\n      \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Modal<\/span>&gt;<\/span><\/span>\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Using Next.js Parallel and Intercepting routes, we can also use browser navigation to close and re-open the image modal:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1712622502\/Blog-Building_an_Image_Gallery_with_Next.js_Parallel_and_Intercepting_Routes-9.png\" alt=\"next.js image gallery example demo\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"729\" height=\"579\"\/><\/p>\n<p>All the images have a shareable URL that you can copy and paste into your browser to open the image page, as you can see in the example above.<\/p>\n<p>The actual photo page is:<\/p>\n<p><code>app\/photos\/[id]\/page.tsx:<\/code><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">PhotoPage<\/span>(<span class=\"hljs-params\">{\n  params: { id: photoId },\n}: {\n  params: { id: string };\n}<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> picture = pictures.find(<span class=\"hljs-function\">(<span class=\"hljs-params\">{ id }<\/span>) =&gt;<\/span> id === photoId);\n  <span class=\"hljs-keyword\">if<\/span> (!picture) <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>No Image found<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldImage<\/span>\n        <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{picture.image.publicId}<\/span>\n        <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">{730}<\/span>\n        <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">{900}<\/span>\n        <span class=\"hljs-attr\">crop<\/span>=<span class=\"hljs-string\">\"fill\"<\/span>\n        <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">{picture.image.alt}<\/span>\n      \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h2>Conclusion<\/h2>\n<p>By harnessing the capabilities of Next.js\u2019s Intercepting and Parallel routes and seamlessly integrating them with Cloudinary Digital Asset Management features, we\u2019ve successfully constructed a robust and dynamic image gallery. Our exploration only scratches the surface of Cloudinary Digital Asset Management, which enables us to optimize image usage, directly perform image transformation, both manually and with generative AI, and explore a lot of other functionalities.<\/p>\n<p>If you found this blog post helpful and want to discuss it in more detail, head over to the <a href=\"https:\/\/community.cloudinary.com\/\">Cloudinary Community forum<\/a> and its associated <a href=\"https:\/\/community.cloudinary.com\/\">Discord<\/a>.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":33645,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[89,370,212],"class_list":["post-33603","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-digital-asset-management","tag-image","tag-next-js"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Creating Dynamic Image Galleries With Cloudinary and Next.js<\/title>\n<meta name=\"description\" content=\"Build interactive image galleries using Cloudinary Digital Asset Management solutions &amp; Next.js&#039;s parallel &amp; intercepting routes to enhance UI design.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Build an Image Gallery: Next.js Parallel and Intercepting Routes\" \/>\n<meta property=\"og:description\" content=\"Build interactive image galleries using Cloudinary Digital Asset Management solutions &amp; Next.js&#039;s parallel &amp; intercepting routes to enhance UI design.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2024-04-30T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-04-16T20:14:55+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Build an Image Gallery: Next.js Parallel and Intercepting Routes\",\"datePublished\":\"2024-04-30T14:00:00+00:00\",\"dateModified\":\"2025-04-16T20:14:55+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\"},\"wordCount\":10,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA\",\"keywords\":[\"Digital Asset Management\",\"Image\",\"Next.js\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2024\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\",\"url\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\",\"name\":\"Creating Dynamic Image Galleries With Cloudinary and Next.js\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA\",\"datePublished\":\"2024-04-30T14:00:00+00:00\",\"dateModified\":\"2025-04-16T20:14:55+00:00\",\"description\":\"Build interactive image galleries using Cloudinary Digital Asset Management solutions & Next.js's parallel & intercepting routes to enhance UI design.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build an Image Gallery: Next.js Parallel and Intercepting Routes\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Creating Dynamic Image Galleries With Cloudinary and Next.js","description":"Build interactive image galleries using Cloudinary Digital Asset Management solutions & Next.js's parallel & intercepting routes to enhance UI design.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes","og_locale":"en_US","og_type":"article","og_title":"Build an Image Gallery: Next.js Parallel and Intercepting Routes","og_description":"Build interactive image galleries using Cloudinary Digital Asset Management solutions & Next.js's parallel & intercepting routes to enhance UI design.","og_url":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes","og_site_name":"Cloudinary Blog","article_published_time":"2024-04-30T14:00:00+00:00","article_modified_time":"2025-04-16T20:14:55+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog-jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Build an Image Gallery: Next.js Parallel and Intercepting Routes","datePublished":"2024-04-30T14:00:00+00:00","dateModified":"2025-04-16T20:14:55+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes"},"wordCount":10,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA","keywords":["Digital Asset Management","Image","Next.js"],"inLanguage":"en-US","copyrightYear":"2024","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes","url":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes","name":"Creating Dynamic Image Galleries With Cloudinary and Next.js","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA","datePublished":"2024-04-30T14:00:00+00:00","dateModified":"2025-04-16T20:14:55+00:00","description":"Build interactive image galleries using Cloudinary Digital Asset Management solutions & Next.js's parallel & intercepting routes to enhance UI design.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/image-gallery-next-js-parallel-intercepting-routes#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Build an Image Gallery: Next.js Parallel and Intercepting Routes"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1713378097\/image_gallery_NextJS-blog\/image_gallery_NextJS-blog.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/33603","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=33603"}],"version-history":[{"count":12,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/33603\/revisions"}],"predecessor-version":[{"id":37446,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/33603\/revisions\/37446"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/33645"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=33603"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=33603"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=33603"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}