Next.js Intercepting Routes: Why This Codebase Has Zero
Intercepting routes mask navigation to a route in your own app. This storefront's live-preview modal shows a cross-origin iframe instead — a real reason the pattern never fit here.
An intercepting route lets Next.js render a route from elsewhere in your app as an overlay on the current page, while the URL updates to match — refresh, and the same path renders as its own full page instead. It's the mechanism behind a photo grid that opens /photo/3 as a modal on top of /feed but shows it full-screen on a direct visit. This storefront has never used it, and the reason is architectural, not an oversight: every modal here shows content that isn't a route in this app at all.
What it looks like, and why the answer is "not here"
The convention is a folder prefix — (.), (..), (..)(..), or (...) — naming which segment's slot to intercept, paired with a parallel-routes @slot in the shared layout:
app/
feed/
layout.tsx # renders {children} and {modal}
page.tsx
@modal/
default.tsx # nothing, when no photo is open
(.)photo/[id]/
page.tsx # renders INSIDE @modal when reached via a <Link> from /feed
photo/[id]/
page.tsx # the full page, reached on a direct visit or a refresh
// app/feed/layout.tsx
export default function FeedLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}
Click a <Link href="/photo/3"> from inside /feed and Next.js renders app/feed/@modal/(.)photo/[id]/page.tsx into the modal slot — the URL becomes /photo/3, the feed stays mounted underneath, and the browser's back button closes it. Paste /photo/3 directly into the address bar, and there's no /feed navigation to intercept, so Next.js renders app/photo/[id]/page.tsx — the plain full page — instead. Same URL, two render paths, chosen by how you arrived.
Every example of this pattern — a photo lightbox, a login modal, a product quick-view — shares one property this storefront's own modals don't have: the content behind the overlay is another route in the same app, reachable and shareable on its own. greping this codebase's src/app tree for the (.)/(..)/(...) folder-naming convention or an @-prefixed parallel-route slot returns nothing — zero matches, across every route this site ships.
The modal this storefront actually built, and why it couldn't be an intercepted route
PreviewModal.tsx is the component every template card, preview button, and edition picker opens to show a live demo. It's a plain client component gated by boolean state:
// src/components/molecules/PreviewModal.tsx
export default function PreviewModal({
open,
onClose,
title,
url,
actions,
}: {
open: boolean;
onClose: () => void;
title: string;
url: string;
actions?: ReactNode;
}) {
// ...
if (!open) return null;
return (
<div aria-label={`${title} live preview`} aria-modal="true" role="dialog" ...>
{/* device-width toggle, focus trap, iframe */}
</div>
);
}
open/onClose are lifted state from whichever card rendered the trigger — TemplateCard, UseCaseCard, EditionPicker, ProductDownloadGroup all call it the same way. There's a structural reason this can't be an intercepted route rather than a state toggle: the url it renders in an <iframe> is the template's own live preview — a Vercel deployment on an entirely different domain from this storefront, per-product, per-edition. Intercepting routes mask navigation to a route inside this Next.js app; there is no /preview/[slug] page in src/app for (.)preview/[slug] to intercept, because the content being shown was never our route to begin with. It's a cross-origin document, loaded the same way regardless of how the visitor arrived, with no "full-page" fallback to render on a direct hit because there is no shareable URL for "the preview modal, open" — only for the product page it launches from.
SavedTemplates.tsx, the other role="dialog" component in this codebase (the wishlist drawer), makes the same case from a different angle: its content — a filtered slice of the visitor's own localStorage-backed wishlist — has no server-side route to render at all, intercepted or not. Read across this codebase's actual overlay components (PreviewModal, SavedTemplates, and the EditionPicker/DownloadMenu dropdown pickers beside them), the count is consistent: state-driven overlays everywhere, zero interception conventions, because none of their content is a URL this app owns.
When the pattern would actually fit this codebase
The honest counter-case is a feature this site doesn't have: a /templates/[slug] quick-view opened from /templates, where the "full page" is one of this app's own routes. That would intercept cleanly —
app/
templates/
layout.tsx
page.tsx
@quickview/
default.tsx
(.)[slug]/
page.tsx # renders inside @quickview from a card click
[slug]/
page.tsx # the real product page, same component either way
— because both render paths point at content this app actually serves. The difference from PreviewModal is exactly the one above: a quick-view of /templates/asoc-haven-landing is a route in this app with a real, shareable, refreshable URL. A live iframe of a template's own hosted demo is not, and building a fake internal route just to intercept it ((.)preview/[slug] rendering an <iframe> to an external URL) would add a route this app doesn't otherwise need, for a URL nobody should bookmark as this site's — the shareable link a visitor wants is the product page, which is exactly what already happens today.
Intercepting routes vs. parallel routes, since the docs pair them
The two features are often introduced together because intercepting routes are usually built on a parallel route's @slot, but they answer different questions. A parallel route alone lets a layout render more than one page simultaneously — a dashboard with an @analytics slot and an @team slot rendering side by side, both reachable by their own navigation, neither one "inside" the other. Intercepting routes add the masking behavior on top: a (.) segment inside a slot doesn't just render alongside the main content, it hijacks the navigation for a specific route so that route renders into the slot instead of replacing the page. This codebase has no @-prefixed slots of either kind — no page here needs two independent views rendered at once, let alone one that also needs to intercept navigation into it. The absence is doubled, not just the interception half.
Mistakes and how they show up
| Mistake | Symptom | Fix |
|---|---|---|
| Reaching for intercepting routes for any modal | Adding @slot folders and (.) conventions for content that isn't a route in your app at all | Ask first whether the overlay's content has its own shareable URL in this app — if not, it's open/onClose state, not a route |
Forgetting the default.tsx in a parallel-routes slot | A hard refresh on any sibling route 404s, because Next.js has nothing to render into the unmatched slot | Always ship a default.tsx that renders null (or nothing) for the closed state |
| Assuming the intercepted route needs its own separate component | Duplicated markup between the modal version and the full-page version | Share one page component; the intercepting route and the real route can both render it |
| Intercepting a route that's also gated by middleware/auth | The modal renders successfully, then a direct refresh redirects — inconsistent behavior between the two paths | Make sure both render paths pass through the same access checks |
| Building a same-app placeholder route just to intercept external content | An extra route this app doesn't otherwise need, and a URL nobody should treat as canonical | If the content is genuinely external (a cross-origin iframe, a different app's page), use client state, not interception |
Frequently asked questions
Does an intercepted route need generateStaticParams like a normal dynamic route?
Yes — the intercepted segment is still a route with its own params, and if the destination route uses static generation, the intercepting version typically shares the same data-fetching, just rendered into a different slot.
Can intercepting routes work with a fully static (SSG) site?
Yes, in principle — nothing about the (.) convention forces dynamic rendering on its own. What forces a route dynamic is unrelated: reading cookies, using searchParams at request time, or similar — the same triggers covered in this storefront's own static-rendering audit, which found zero intercepting-route usage among its actual triggers because, as above, none exist in this codebase.
What's the difference between this and a plain client-side modal?
A plain modal (this storefront's PreviewModal) is open/onClose state with no URL change — refresh the page and the modal is gone, because nothing about its content was ever addressable. An intercepted route changes the URL to match the content shown, so the same state is recoverable on refresh or shared as a link, because the content genuinely is a route.
Why not fake an internal route just so the preview modal could use this pattern?
Because the URL that would create isn't one anybody should bookmark — the demo lives on the template's own preview deployment, not this app, and the shareable link a visitor actually wants is the product page (/templates/<slug>), which the current implementation already serves as the real, refreshable URL underneath the modal.
Templates in this post
ASoc Estate Admin is a real-estate management dashboard with three dashboards and full agent/listing modules — a workspace-app-heavy admin where a properly scoped PreviewModal-style overlay (rather than a route interception) is exactly the right tool for a live-preview button. ASoc Lura is the multi-vertical admin suite — eleven dashboards across roughly 177 pages — the largest single product in the catalog and a case where quick-view-style overlays would matter most if this storefront ever added same-app previews. ASoc Pulse Admin is a commerce-ops dashboard with five dashboards and a full store back office.
Browse the full sets: React admin templates, Next.js admin templates, Tailwind admin templates. For the fuller list of what pushes a Next.js route off static rendering, the static-rendering audit; for the modal's own focus-trap and device-scaling implementation, this storefront's product-gallery post covers the neighboring crawlability question for carousel content.
