0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Ders

Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma

Öğeleri kalıpta açıp yenilemede derin bağlantı verebilmek için araya giren ve paralel rotaları birleştirin.

Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma, CoddyKit'te ücretsiz bir Next.js 15 Fullstack (App Router + Server Actions) dersidir. Bu, 4 dersinin 4. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, Next.js 15 Fullstack (App Router + Server Actions) öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.

Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.

Why Modals Break Deep-Linking

Traditional modal implementations render a dialog on top of the current page. This creates a fundamental problem: when a user shares the URL or refreshes the browser, the modal disappears — there is no route change, so no persistent URL state exists.

Next.js 15 solves this with two advanced routing features working in tandem:

  • Intercepting Routes — catch a navigation and render a different UI in the current context
  • Parallel Routes — render two route segments simultaneously in the same layout

Together they let you open a photo in a modal with a proper URL (e.g. /photos/42) while keeping the grid visible behind it. On a hard refresh, the same URL shows the full-page photo view instead.

The File Structure You Need

The pattern requires a specific directory layout inside your app/ folder. Here is the structure for a photo gallery:

  • app/photos/page.tsx — the photo grid (list view)
  • app/photos/[id]/page.tsx — the standalone full-page photo detail
  • app/@modal/(.)photos/[id]/page.tsx — the intercepted modal version
  • app/layout.tsx — root layout that accepts the modal slot

The @modal prefix declares a parallel route slot. The (.) prefix on the intercepting segment tells Next.js: intercept navigations that go to photos/[id] at the same depth in the URL tree.

This dual existence — a real page at photos/[id] and an intercepted version at @modal/(.)photos/[id] — is the entire secret.

Configuring the Root Layout for Parallel Slots

The root layout (or any shared layout) must be updated to accept and render the modal slot. Next.js passes each parallel route slot as a named prop to the layout component.

If no intercepting route is active, Next.js renders a built-in default.tsx file for the slot. Without it the app crashes — always provide a default.tsx returning null.

// app/layout.tsx
import type { ReactNode } from 'react';

interface RootLayoutProps {
  children: ReactNode;
  modal: ReactNode; // named slot from @modal parallel route
}

export default function RootLayout({ children, modal }: RootLayoutProps) {
  return (
    <html lang="en">
      <body>
        {children}
        {modal} {/* renders null by default, or the intercepted modal */}
      </body>
    </html>
  );
}

// app/@modal/default.tsx
export default function ModalDefault() {
  return null; // no modal active — render nothing
}

The Photo Grid Page

The grid page at app/photos/page.tsx fetches all photos and renders them as links. When a user clicks a photo, Next.js detects that the destination (/photos/[id]) has an intercepting counterpart and silently redirects the render to @modal/(.)photos/[id] instead — without changing the displayed URL behavior.

Key points:

  • Use a plain <Link href="/photos/42"> — no special modal trigger code needed
  • The interception is entirely file-system driven; the grid component stays clean
  • Marking the page async lets you await your data fetch at the component level
// app/photos/page.tsx
import Link from 'next/link';
import { getAllPhotos } from '@/lib/photos';

export default async function PhotosPage() {
  const photos = await getAllPhotos();

  return (
    <main className="grid grid-cols-3 gap-4 p-6">
      {photos.map((photo) => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img
            src={photo.thumbnailUrl}
            alt={photo.title}
            className="w-full aspect-square object-cover rounded-lg hover:opacity-80 transition"
          />
        </Link>
      ))}
    </main>
  );
}

The Full-Page Photo Detail (Hard Refresh Target)

The route at app/photos/[id]/page.tsx is the canonical destination. It renders when:

  • The user pastes the URL directly in the address bar
  • The user refreshes the page while the modal is open
  • A search engine crawler indexes the photo

This page is a standard Next.js async Server Component — it receives the params prop and fetches its own data. No special intercepting-route knowledge is needed here.

// app/photos/[id]/page.tsx
import { getPhotoById } from '@/lib/photos';
import { notFound } from 'next/navigation';

interface PhotoPageProps {
  params: Promise<{ id: string }>;
}

export default async function PhotoPage({ params }: PhotoPageProps) {
  const { id } = await params; // params is a Promise in Next.js 15
  const photo = await getPhotoById(id);

  if (!photo) notFound();

  return (
    <main className="max-w-3xl mx-auto p-8">
      <img src={photo.imageUrl} alt={photo.title} className="w-full rounded-xl" />
      <h1 className="mt-4 text-2xl font-bold">{photo.title}</h1>
      <p className="mt-2 text-gray-600">{photo.description}</p>
    </main>
  );
}

The Intercepting Modal Route

The file at app/@modal/(.)photos/[id]/page.tsx is rendered instead of the full-page view when navigation happens client-side from the grid. The (.) segment means intercept one level up — matching photos/[id] relative to the current route tree depth.

Interception depth notation:

  • (.) — same level
  • (..) — one level up
  • (...) — from the root

This component renders a dialog overlay and reuses the same data-fetching logic as the full-page view, keeping content consistent regardless of how the user arrived.

// app/@modal/(.)photos/[id]/page.tsx
import { getPhotoById } from '@/lib/photos';
import { notFound } from 'next/navigation';
import PhotoModal from '@/components/PhotoModal';

interface InterceptedPhotoPageProps {
  params: Promise<{ id: string }>;
}

export default async function InterceptedPhotoPage({
  params,
}: InterceptedPhotoPageProps) {
  const { id } = await params;
  const photo = await getPhotoById(id);

  if (!photo) notFound();

  return <PhotoModal photo={photo} />;
}

Building the Modal Client Component

The PhotoModal component needs to close the modal when the user clicks the backdrop or a close button. In a parallel + intercepting route setup, closing means navigating back — which restores the underlying grid and clears the modal slot.

Use useRouter().back() from next/navigation for this. Mark the component 'use client' since it uses browser hooks. The dialog element uses the native HTML <dialog> API for accessible modal semantics.

'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
import type { Photo } from '@/lib/photos';

interface PhotoModalProps {
  photo: Photo;
}

export default function PhotoModal({ photo }: PhotoModalProps) {
  const router = useRouter();
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    dialogRef.current?.showModal();
  }, []);

  function handleClose() {
    router.back(); // removes modal from parallel slot
  }

  return (
    <dialog
      ref={dialogRef}
      onClose={handleClose}
      className="backdrop:bg-black/60 rounded-2xl p-0 max-w-2xl w-full"
    >
      <button
        onClick={handleClose}
        className="absolute top-3 right-3 text-white text-xl"
        aria-label="Close"
      >
        ✕
      </button>
      <img src={photo.imageUrl} alt={photo.title} className="w-full rounded-t-2xl" />
      <div className="p-6">
        <h2 className="text-xl font-bold">{photo.title}</h2>
        <p className="mt-2 text-gray-600">{photo.description}</p>
      </div>
    </dialog>
  );
}

Adding a Loading State for the Modal Slot

Because the intercepting route fetches data server-side before streaming to the client, there can be a visible delay. Next.js supports co-located loading.tsx files inside parallel route slots to show a skeleton while the server component resolves.

Place it at app/@modal/(.)photos/[id]/loading.tsx. This follows the same Suspense-based streaming that applies to all route segments — parallel slots are no exception.

// app/@modal/(.)photos/[id]/loading.tsx
export default function ModalLoading() {
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
      <div className="bg-white rounded-2xl p-8 max-w-2xl w-full animate-pulse">
        <div className="h-72 bg-gray-200 rounded-xl mb-4" />
        <div className="h-6 bg-gray-200 rounded w-2/3 mb-2" />
        <div className="h-4 bg-gray-100 rounded w-full" />
      </div>
    </div>
  );
}

Handling Error Boundaries in the Modal Slot

If the photo fetch fails (e.g. invalid ID, network error), the intercepted route should degrade gracefully. Add an error.tsx file inside the intercepting segment — it must be a Client Component because it receives the error object and a reset function.

The error boundary is scoped to the parallel slot, so an error in the modal does not crash the photo grid behind it.

'use client';

// app/@modal/(.)photos/[id]/error.tsx
import { useRouter } from 'next/navigation';

interface ModalErrorProps {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function ModalError({ error, reset }: ModalErrorProps) {
  const router = useRouter();

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
      <div className="bg-white rounded-2xl p-8 text-center">
        <h2 className="text-lg font-semibold text-red-600">Failed to load photo</h2>
        <p className="mt-2 text-sm text-gray-500">{error.message}</p>
        <div className="mt-4 flex gap-3 justify-center">
          <button onClick={reset} className="px-4 py-2 bg-blue-500 text-white rounded-lg">
            Try again
          </button>
          <button onClick={() => router.back()} className="px-4 py-2 bg-gray-200 rounded-lg">
            Go back
          </button>
        </div>
      </div>
    </div>
  );
}

Generating Metadata for Social Sharing

One major benefit of this pattern is that the full-page route at app/photos/[id]/page.tsx can export rich Open Graph metadata. When a user shares the URL, crawlers hit the canonical route — not the modal — and receive proper <meta> tags.

Export a generateMetadata function from the full-page route. The intercepted modal page does not need its own metadata export because crawlers never reach it directly.

// app/photos/[id]/page.tsx  (metadata addition)
import type { Metadata } from 'next';
import { getPhotoById } from '@/lib/photos';

interface Props {
  params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const photo = await getPhotoById(id);

  if (!photo) return { title: 'Photo not found' };

  return {
    title: photo.title,
    description: photo.description,
    openGraph: {
      title: photo.title,
      description: photo.description,
      images: [{ url: photo.imageUrl, width: 1200, height: 630 }],
    },
  };
}

Testing the Soft Navigation vs Hard Refresh Behaviour

Once the structure is in place, manually verify the two distinct code paths:

  • Soft navigation (expected: modal): Start on /photos, click any image. The URL changes to /photos/42 but the grid remains visible beneath the modal overlay. The @modal slot is active.
  • Hard refresh (expected: full page): While the modal is open, press F5 or Cmd+R. The page reloads and app/photos/[id]/page.tsx renders in full — no grid, no modal wrapper — as if you navigated directly.
  • Back navigation (expected: grid): Click the close button or browser back. router.back() pops the history entry, the modal slot clears, and the grid reappears without a full reload.

This three-way behaviour is the defining property of the combined pattern and should be your primary acceptance criterion during development.

Quick Check: Intercepting Route Depth Notation

You are building a dashboard at app/dashboard/ that contains a team list at app/dashboard/team/[memberId]/page.tsx. You want to intercept navigations to a member's profile and show it in a modal while keeping the dashboard visible. Where should you place the intercepting route file?

Recap: The Complete Photo-Modal Pattern

In this lesson you built a full photo gallery with soft-navigation modals and deep-linkable URLs using two complementary Next.js 15 features:

  • Parallel Routes (@modal) — create a named slot in your layout that can render independently alongside children, defaulting to null via default.tsx
  • Intercepting Routes ((.), (..), (...)) — catch a client-side navigation and substitute a different component without changing the URL

The key insights to remember:

  • The canonical route (photos/[id]/page.tsx) always handles hard refreshes and search engine crawls — keep it complete with metadata
  • The intercepting route (@modal/(.)photos/[id]/page.tsx) only activates on soft navigation from within the app
  • Closing the modal calls router.back() — no custom state management required
  • Add loading.tsx and error.tsx inside the intercepting segment to handle async states gracefully

This pattern delivers the user experience of a single-page app while preserving the shareability and SEO benefits of a multi-page architecture.

Sıkça Sorulan Sorular

“Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma” dersi ücretsiz mi?

Evet — “Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve Next.js 15 Fullstack (App Router + Server Actions) kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.

“Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma” dersinde ne öğreneceğim?

Öğeleri kalıpta açıp yenilemede derin bağlantı verebilmek için araya giren ve paralel rotaları birleştirin. Next.js 15 Fullstack (App Router + Server Actions) ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.

Next.js 15 Fullstack (App Router + Server Actions) öğrenmeye başlamak için deneyim gerekli mi?

Önceden deneyim gerekmez. CoddyKit'te Next.js 15 Fullstack (App Router + Server Actions), başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 4. dersidir.

“Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma” dersi ne kadar sürer?

Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.

Bu Next.js 15 Fullstack (App Router + Server Actions) dersinde kod yazıp çalıştırabilir miyim?

Evet. Her Next.js 15 Fullstack (App Router + Server Actions) dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.

Bu kursun tüm dersleri

  1. Adlandırılmış Yuvalar ve Varsayılan Bölümlerle Paralel Rotalar
  2. Paylaşılan Kalıp Deneyimleri için Araya Giren Rotalar
  3. Panolar ve Sekmeler için Koşullu Yuva Oluşturma
  4. Yumuşak Gezinmeyle Fotoğraf Kalıbı Akışı Oluşturma
← Next.js 15 Fullstack (App Router + Server Actions) Sayfasına Dön