المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة
استخدم اصطلاحي (.) و(..) لعرض المحتوى كنافذة منبثقة مع الحفاظ على عنوان URL قابل للمشاركة.
المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة درس مجاني في Next.js 15 Fullstack (App Router + Server Actions) على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Next.js 15 Fullstack (App Router + Server Actions)، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Next.js 15 Fullstack (App Router + Server Actions) 4 دروس في المجموع.
بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.
What Are Intercepting Routes?
Next.js 15 introduces intercepting routes — a routing convention that lets you load a route within the context of the current layout, rather than navigating to a completely new page.
The most common use case is a modal pattern: clicking a photo in a grid shows it in a modal overlay, but the URL updates to /photos/42. If you share that URL, the recipient sees the full photo page — not the modal.
- The browser URL changes (shareable, bookmarkable)
- The current page stays visible behind the modal
- Hard refresh or direct navigation loads the full standalone page
This is sometimes called the modal soft-navigation pattern, popularised by Pinterest, Instagram, and Vercel's own site.
The Interception Conventions: (.) and (..)
Next.js uses folder-name prefixes to declare that one route intercepts another. The prefix mirrors relative path syntax:
(.)— intercept a route at the same level in the segment tree(..)— intercept a route one level up(...)— intercept a route from the app root
The folder named with these prefixes lives inside a parallel route slot (a folder prefixed with @), so the two features work together.
Example: to intercept /photos/[id] from the / home page, you create app/@modal/(.)photos/[id]/page.tsx.
Project Structure for the Photo Modal
Let's build a concrete example: a photo gallery where clicking a thumbnail opens a modal, but the URL becomes /photos/42.
The file tree looks like this:
app/
layout.tsx ← root layout with @modal slot
page.tsx ← gallery grid (home)
@modal/
default.tsx ← renders null (slot default)
(.)photos/
[id]/
page.tsx ← intercepted modal UI
photos/
[id]/
page.tsx ← full standalone photo page
When a user clicks a photo from the gallery, Next.js renders @modal/(.)photos/[id]/page.tsx inside the root layout's @modal slot. A hard refresh of /photos/42 renders photos/[id]/page.tsx normally.
Root Layout: Accepting the @modal Slot
The root layout must accept the parallel @modal slot as a prop alongside children. This is how Next.js injects the intercepted route content.
// app/layout.tsx
import type { ReactNode } from 'react';
interface RootLayoutProps {
children: ReactNode;
modal: ReactNode; // injected by the @modal parallel route slot
}
export default function RootLayout({ children, modal }: RootLayoutProps) {
return (
<html lang="en">
<body>
{children}
{modal} {/* renders null by default, modal content when intercepted */}
</body>
</html>
);
}Default Slot: Rendering Nothing When No Modal Is Active
When the user visits / without any interception, Next.js needs to render something for the @modal slot. You provide a default.tsx that returns null.
Without this file, Next.js will throw a 404 because it cannot resolve the slot.
// app/@modal/default.tsx
// This file is required — it tells Next.js to render nothing
// when no route is being intercepted into this slot.
export default function ModalDefault() {
return null;
}The Intercepting Route Page: Modal UI
Now create the intercepted route page. This is what renders inside the @modal slot when a user soft-navigates (via <Link>) to /photos/[id].
It receives the same params as the real page and can fetch the same data. The modal wrapper itself is just a component — you control the overlay styling.
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal';
import { getPhoto } from '@/lib/photos';
interface Props {
params: Promise<{ id: string }>;
}
export default async function PhotoModal({ params }: Props) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img
src={photo.url}
alt={photo.title}
className="max-w-full max-h-screen object-contain"
/>
<h2 className="mt-4 text-lg font-semibold">{photo.title}</h2>
</Modal>
);
}Building the Modal Component with router.back()
The Modal component needs to close itself when the user dismisses it. The idiomatic way is router.back() — this navigates back in history, which removes the intercepted route and restores the gallery page without a full reload.
Use a 'use client' directive because useRouter is a client-side hook.
// components/Modal.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useCallback, type ReactNode } from 'react';
export function Modal({ children }: { children: ReactNode }) {
const router = useRouter();
const close = useCallback(() => router.back(), [router]);
// Close on Escape key
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') close();
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [close]);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70"
onClick={close} // click backdrop to close
>
<div
className="relative bg-white rounded-xl p-6 max-w-2xl w-full"
onClick={(e) => e.stopPropagation()} // prevent backdrop close on content
>
<button
onClick={close}
className="absolute top-3 right-3 text-gray-500 hover:text-gray-900"
aria-label="Close modal"
>
✕
</button>
{children}
</div>
</div>
);
}The Full Photo Page: Direct Navigation Fallback
The standalone /photos/[id] page is what users see when they visit the URL directly — from a shared link, a hard refresh, or a search engine crawler. It renders the complete page layout without any modal wrapper.
This page and the intercepted page can share the same data-fetching function (getPhoto) — no duplication needed for the data layer.
// app/photos/[id]/page.tsx
import { getPhoto } from '@/lib/photos';
import Link from 'next/link';
interface Props {
params: Promise<{ id: string }>;
}
export default async function PhotoPage({ params }: Props) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<main className="container mx-auto py-12">
<Link href="/" className="text-blue-600 hover:underline mb-6 inline-block">
← Back to gallery
</Link>
<img
src={photo.url}
alt={photo.title}
className="w-full rounded-xl shadow-lg"
/>
<h1 className="mt-6 text-3xl font-bold">{photo.title}</h1>
<p className="mt-2 text-gray-600">{photo.description}</p>
</main>
);
}The Gallery Page: Linking Into the Modal
The home gallery page links to /photos/[id] using a standard <Link> component. Next.js detects the intercepting route automatically — you do not need any special prop on the link.
When JavaScript is enabled and the user clicks the link, the interception fires. Without JS (or on first load), the full page loads as normal.
// app/page.tsx
import Link from 'next/link';
import { getPhotos } from '@/lib/photos';
export default async function GalleryPage() {
const photos = await getPhotos();
return (
<main className="container mx-auto py-12">
<h1 className="text-3xl font-bold mb-8">Photo Gallery</h1>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{photos.map((photo) => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<img
src={photo.thumbnailUrl}
alt={photo.title}
className="rounded-lg aspect-square object-cover hover:opacity-90 transition"
/>
</Link>
))}
</div>
</main>
);
}Using (..) to Intercept One Level Up
The (..) prefix intercepts a route that is one segment higher in the URL tree than the slot's location.
Consider a product page at /shop/[category]/[productId]. You want clicking a product inside /shop/[category] to open a modal. The @modal slot lives inside app/shop/[category]/, but the target route is also one level down — so you use (..) to step up and intercept from there.
File path example:
app/shop/[category]/
@modal/
default.tsx
(..)shop/[category]/[productId]/
page.tsx ← intercepts the product page
The rule of thumb: count how many segment levels separate the @modal folder from the target route, and use that many dots.
Loading UI and Suspense Inside Intercepted Routes
Intercepted route pages are async Server Components, so you can wrap them with Suspense to show a loading skeleton while data is fetched — exactly like any other Next.js route.
Add a loading.tsx file next to the intercepted page, or wrap the content in <Suspense fallback={...}> in the modal component itself.
// app/@modal/(.)photos/[id]/loading.tsx
// Shown while the async PhotoModal page is fetching data.
export default function PhotoModalSkeleton() {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
<div className="bg-white rounded-xl p-6 max-w-2xl w-full animate-pulse">
<div className="h-80 bg-gray-200 rounded-lg" />
<div className="mt-4 h-6 bg-gray-200 rounded w-1/2" />
</div>
</div>
);
}Knowledge Check: Which Convention to Use?
Test your understanding of the interception route conventions.
Lesson Recap: Intercepting Routes for Modal Experiences
In this lesson you learned how to deliver the URL-preserving modal pattern in Next.js 15 using intercepting routes.
- (.) convention intercepts a route at the same segment level; (..) steps one level up; (...) goes to the app root.
- Intercepting routes always live inside a parallel route slot (
@modal) so the parent layout can render both the background page and the modal simultaneously. - A default.tsx returning
nullis required in the slot to avoid 404s when no modal is active. - The modal closes with
router.back(), which removes the intercepted route from history without a full reload. - The full standalone page at the real route path still exists — hard refresh and shared links always load the complete page, making the URL truly shareable.
- Add
loading.tsxor Suspense inside the intercepted route to show a skeleton while async data loads.
This pattern gives users a fast, app-like feel while keeping pages SEO-friendly and accessible to anyone with just a link.
الأسئلة الشائعة
هل درس «المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة» مجاني؟
نعم — نص درس «المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Next.js 15 Fullstack (App Router + Server Actions)، انتقل إلى CoddyKit PRO. تتضمن دورة Next.js 15 Fullstack (App Router + Server Actions) 4 دروس في المجموع.
ماذا ستتعلم في «المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة»؟
استخدم اصطلاحي (.) و(..) لعرض المحتوى كنافذة منبثقة مع الحفاظ على عنوان URL قابل للمشاركة. تتمرن على Next.js 15 Fullstack (App Router + Server Actions) مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Next.js 15 Fullstack (App Router + Server Actions)؟
لا تُشترط خبرة سابقة. Next.js 15 Fullstack (App Router + Server Actions) على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Next.js 15 Fullstack (App Router + Server Actions) هذا؟
نعم. كل درس في Next.js 15 Fullstack (App Router + Server Actions) يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- المسارات المتوازية مع الخانات المسماة والمقاطع الافتراضية
- المسارات الاعتراضية لتجارب النوافذ المنبثقة المشتركة
- العرض الشرطي للخانات في لوحات المعلومات وعلامات التبويب
- بناء تدفق نافذة الصور بالتنقل السلس