next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Dynamic intercepting parallel route is server side rendered

Open oleshkooo opened this issue 2 years ago • 9 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.4.0: Mon Mar  6 21:00:41 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T8103
    Binaries:
      Node: 18.12.1
      npm: 9.8.0
      Yarn: 1.22.19
      pnpm: 8.6.0
    Relevant Packages:
      next: 13.4.9
      eslint-config-next: 13.4.10
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 5.1.6
    Next.js Config:
/** @type {import('next').NextConfig} */
const nextConfig = {
    distDir: 'build',
    compiler: {
        removeConsole: process.env.NODE_ENV === 'production',
    },
    images: {
        remotePatterns: [
            {
                protocol: 'https',
                hostname: '**',
            },
        ],
    },
}

module.exports = nextConfig



warn  - Latest canary version not detected, detected: "13.4.9", newest: "13.4.10".
        Please try the latest canary version (`npm install next@canary`) to confirm the issue still exists before creating a new issue.
        Read more - https://nextjs.org/docs/messages/opening-an-issue

(I use version 13.4.9, because in 13.4.10 intercepted parallel routes do not work at all)

Which area(s) of Next.js are affected? (leave empty if unsure)

App Router, Routing (next/router, next/navigation, next/link)

Link to the code that reproduces this issue or a replay of the bug

https://drive.google.com/file/d/1wCb9VkpbAlH1bT-iIcP3dTYnEEOT3Glw/view?usp=sharing

To Reproduce

/src/app/adspaces/[id]/page.tsx

import { AdspaceModalClient } from '@/app/@modal/(.)adspaces/[id]/page.client'
import { prisma } from '@/server/prismaDb'
import { type NextPage } from 'next'
import { notFound } from 'next/navigation'

export const generateStaticParams = async () => {
    const adspaces = await prisma.adspace.findMany({
        select: {
            id: true,
        },
    })
    return adspaces.map(adspace => ({
        id: String(adspace.id),
    }))
}

interface AdspacesProps {
    params: {
        id: string
    }
}
const Adspaces: NextPage<AdspacesProps> = async ({ params }) => {
    const { id } = params
    const adspace = await prisma.adspace.findUnique({
        where: {
            id: Number(id),
        },
        include: {
            sides: true,
        },
    })
    if (!adspace) {
        notFound()
    }

    return <AdspaceModalClient adspace={adspace} />
}

export default Adspaces

/src/app/@modal/(.)adspaces/[id]/page.tsx

import { AdspaceModalClient } from '@/app/@modal/(.)adspaces/[id]/page.client'
import { prisma } from '@/server/prismaDb'
import { type NextPage } from 'next'
import { notFound } from 'next/navigation'

export const generateStaticParams = async () => {
    const adspaces = await prisma.adspace.findMany({
        select: {
            id: true,
        },
    })
    return adspaces.map(adspace => ({
        id: String(adspace.id),
    }))
}

interface AdspacesModalProps {
    params: {
        id: string
    }
}
const AdspacesModal: NextPage<AdspacesModalProps> = async ({ params }) => {
    const { id } = params
    const adspace = await prisma.adspace.findUnique({
        where: {
            id: Number(id),
        },
        include: {
            sides: true,
        },
    })
    if (!adspace) {
        notFound()
    }

    return <AdspaceModalClient adspace={adspace} />
}

export default AdspacesModal

/src/app/layout.tsx

import { TailwindIndicator } from '@/components/tailwind-indicator'
import { Toaster } from '@/components/ui/toaster'
import { WebVitals } from '@/components/web-vitals'
import { websiteMetadata } from '@/config/metadata.config'
import '@/styles/global.css'
import { Metadata } from 'next'
import { Inter } from 'next/font/google'

export const metadata: Metadata = websiteMetadata

const font = Inter({
    subsets: ['latin', 'cyrillic', 'cyrillic-ext'],
    preload: false,
})

interface RootLayoutProps {
    children: React.ReactNode
    modal: React.ReactNode
}
const RootLayout: React.FC<RootLayoutProps> = props => {
    return (
        <html lang="en">
            <body style={font.style}>
                {props.children}
                {props.modal}
                {/*  */}
                <Toaster />
                <TailwindIndicator />
                <WebVitals />
            </body>
        </html>
    )
}

export default RootLayout

Describe the Bug

/adspaces/[id] builds normally (SSG), but when I do the same in /(.)adspaces/[id], it somehow becomes SSR, even though each of these pages exports the same generateStaticParams function.

$ next build
- info Loaded env from /Users/oleh/Desktop/advertize-new/.env.production
- info Loaded env from /Users/oleh/Desktop/advertize-new/.env
- info Creating an optimized production build  
- info Compiled successfully
- info Linting and checking validity of types  
- info Collecting page data  
[    ] - info Generating static pages (0/53)- warn Entire page /order deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering /order
- warn Entire page /(.)order deopted into client-side rendering. https://nextjs.org/docs/messages/deopted-into-client-rendering /(.)order
- info Generating static pages (53/53)
- info Finalizing page optimization  

Route (app)                                Size     First Load JS
┌ ○ /                                      13.8 kB         134 kB
├ λ /(.)adspaces/[id]                      2.11 kB         117 kB
├ ○ /(.)order                              1.32 kB         161 kB
├ ● /adspaces/[id]                         2.11 kB         117 kB
├   ├ /adspaces/1
├   ├ /adspaces/2
├   ├ /adspaces/3
├   └ [+37 more paths]
├ λ /api/order                             0 B                0 B
├ λ /api/revalidate                        0 B                0 B
├ ○ /favicon.ico                           0 B                0 B
├ ○ /manifest.webmanifest                  0 B                0 B
├ ○ /opengraph-image.png                   0 B                0 B
├ ○ /order                                 2.4 kB          167 kB
├ ○ /robots.txt                            0 B                0 B
├ ○ /sitemap.xml                           0 B                0 B
└ ○ /twitter-image.png                     0 B                0 B
+ First Load JS shared by all              77.6 kB
  ├ chunks/698-fa1560bd3d687c07.js         25 kB
  ├ chunks/bce60fc1-1099bf5e527a55df.js    50.5 kB
  ├ chunks/main-app-5c31dc370cc81b80.js    212 B
  └ chunks/webpack-3994cc1df7b93e85.js     1.81 kB

Route (pages)                              Size     First Load JS
─ ○ /404                                   181 B          79.4 kB
+ First Load JS shared by all              79.2 kB
  ├ chunks/framework-8883d1e9be70c3da.js   45 kB
  ├ chunks/main-39568768d6412e27.js        32.2 kB
  ├ chunks/pages/_app-b75b9482ff6ea491.js  195 B
  └ chunks/webpack-3994cc1df7b93e85.js     1.81 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

Expected Behavior

Expected /(.)adspaces/[id] page to be SSG as well. It's something like this:

$ next build

Route (app)                                Size     First Load JS
┌ ○ /                                      13.8 kB         134 kB
├ ● /(.)adspaces/[id]                      2.11 kB         117 kB
├   ├ /adspaces/1
├   ├ /adspaces/2
├   ├ /adspaces/3
├   └ [+37 more paths]
├ ○ /(.)order                              1.32 kB         161 kB
├ ● /adspaces/[id]                         2.11 kB         117 kB
├   ├ /adspaces/1
├   ├ /adspaces/2
├   ├ /adspaces/3
├   └ [+37 more paths]
├ λ /api/order                             0 B                0 B
├ λ /api/revalidate                        0 B                0 B
├ ○ /favicon.ico                           0 B                0 B
├ ○ /manifest.webmanifest                  0 B                0 B
├ ○ /opengraph-image.png                   0 B                0 B
├ ○ /order                                 2.4 kB          167 kB
├ ○ /robots.txt                            0 B                0 B
├ ○ /sitemap.xml                           0 B                0 B
└ ○ /twitter-image.png                     0 B                0 B
+ First Load JS shared by all              77.6 kB
  ├ chunks/698-fa1560bd3d687c07.js         25 kB
  ├ chunks/bce60fc1-1099bf5e527a55df.js    50.5 kB
  ├ chunks/main-app-5c31dc370cc81b80.js    212 B
  └ chunks/webpack-3994cc1df7b93e85.js     1.81 kB

Route (pages)                              Size     First Load JS
─ ○ /404                                   181 B          79.4 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

Which browser are you using? (if relevant)

Brave, Chrome

How are you deploying your application? (if relevant)

Firebase hosting + firebase functions (firebase does it on its own, I use experimental firebase frameworks)

oleshkooo avatar Jul 18 '23 17:07 oleshkooo

+1 on this! @Oleshkooo Have you found the issue here?

lukasjoho avatar Aug 21 '23 15:08 lukasjoho

Yep seems like generateStaticParams is not triggering for pages inside @slot

Jordaneisenburger avatar Aug 22 '23 12:08 Jordaneisenburger

+1 on this! @Oleshkooo Have you found the issue here?

Accidentally closed the issue*

Since I couldn't implement my idea at the time, I just had to delete those pages. Now I have updated the "next" version, but I haven't tested it yet. If there are any updates, I'll let you know. I will also be glad to hear about any changes and, hopefully, successes.

oleshkooo avatar Aug 22 '23 22:08 oleshkooo

Yep seems like generateStaticParams is not triggering for pages inside @slot

Facing this issue, and end up by replace parallel route (@slot things) to layout.tsx.

AS-IS:

.
├── @slot1
│   ├── [slug]
│   │   └── page.tsx      # generateStaticParams doesn't work as expected
│   └── page.tsx          # export default () => null;
├── @slot2
│   └── default.tsx
├── layout.tsx            # export default ({ slot1, slot2 }) => (<>{slot1}{slot2}</>);
└── page.tsx              # export default () => null;

TO-BE:

.
├── [slug]              # -> @slot1/[slug]
│   └── page.tsx          # generateStaticParams works as expected
├── Slot2Component.tsx  # -> @slot2/default.tsx
├── layout.tsx            # export default ({ children }) => (<>{chidlren}<Slot2Component /></>);
└── page.tsx              # export default () => null;

Actually I'm also using this with route group ((group) things) that can make multiple layout.tsx for single route. This approach makes so many annoying folders and layout.tsx but yeah... works correctly at least

ramram1048 avatar Sep 13 '23 03:09 ramram1048

Still relevant on Next.js 14.

Raberrse avatar Dec 30 '23 07:12 Raberrse

@Oleshkooo Unfortunately, this is not a minimal :repro: that we can take look. If you can create a minimal :public-big: :repro:, we will be able to take a look at this!

samcx avatar Jan 10 '24 21:01 samcx

I made a fork of the official example nextgram so it's easier to reproduce, the project is already using "next": "canary".

My Fork:

git clone https://github.com/Jaycedam/nextgram.git

I added the same generateStaticParams() to the route and the modal (single commit). As mentioned in the issue, only the route is SSG, the modal is still dynamic.

Build result:

Route (app)                              Size     First Load JS
┌ ○ /                                    6.94 kB        91.1 kB
├ ○ /_not-found                          885 B          85.1 kB
├ λ /(.)photos/[id]                      481 B          84.7 kB
├ ○ /default                             143 B          84.3 kB
└ ● /photos/[id]                         143 B          84.3 kB
    ├ /photos/1
    ├ /photos/2
    ├ /photos/3
    └ [+3 more paths]
+ First Load JS shared by all            84.2 kB
  ├ chunks/69-ba1ea0421cdf6c89.js        28.9 kB
  ├ chunks/fd9d1056-0bb21fb122762d6f.js  53.4 kB
  └ other shared chunks (total)          1.86 kB


○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
λ  (Dynamic)  server-rendered on demand using Node.js

Jaycedam avatar Feb 02 '24 00:02 Jaycedam

@Jaycedam Thank you for sharing!

I can confirm that this is indeed an issue. We will be looking to fix this!

samcx avatar Feb 05 '24 01:02 samcx

Reproducible in "next": "^14.1.2"

EDIT: I don't know if this is of any help, but here is some of my findings:

  • This condition is not met: https://github.com/vercel/next.js/blob/6b111ec6d33d21dc9a47864dc5fa23ed120cb2a6/packages/next/src/build/index.ts#L2087
  • During debugging it seemed like workerResult.prerenderRoutes has no objects in it.
  • staticInfo a bit earlier in the code does say generateStaticParams: true

KoenLemmen avatar Mar 06 '24 20:03 KoenLemmen

My page heavily uses parallel routing, which ultimately ends in just having dynamic rendering and no SSR at all.

So adding this feature would be really beneficial -> https://patrick-arns.de/

PArns avatar Apr 18 '24 20:04 PArns

It seems that interception routes were deliberately excluded from static builds: https://github.com/vercel/next.js/pull/61004

I use a CDN in front of my self-hosted setup. This issue creates a problem when the parent page is statically rendered and thus cached by CDN, but the intercepted route (modal) isn't. Therefore after each deployment, I have to clear the full cache since the interception no longer works, and falls back to hard load.

fmnxl avatar Jul 05 '24 15:07 fmnxl