diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 12292fbf3cd80..7b8c1843b6a4c 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -1322,6 +1322,16 @@ export default abstract class Server { opts.isBot = isBotRequest } + // In development, we always want to generate dynamic HTML. + if ( + !isDataReq && + isAppPath && + opts.dev && + opts.supportsDynamicHTML === false + ) { + opts.supportsDynamicHTML = true + } + const defaultLocale = isSSG ? this.nextConfig.i18n?.defaultLocale : query.__nextDefaultLocale @@ -1446,7 +1456,9 @@ export default abstract class Server { let isRevalidate = false const doRender: () => Promise = async () => { - const supportsDynamicHTML = !(isSSG || hasStaticPaths) + // In development, we always want to generate dynamic HTML. + const supportsDynamicHTML = + (!isDataReq && opts.dev) || !(isSSG || hasStaticPaths) const match = pathname !== '/_error' && !is404Page && !is500Page @@ -1667,43 +1679,45 @@ export default abstract class Server { fallbackMode = 'blocking' } + // We use `ssgCacheKey` here as it is normalized to match the encoding + // from getStaticPaths along with including the locale. + // + // We use the `resolvedUrlPathname` for the development case when this + // is an app path since it doesn't include locale information. + let staticPathKey = + ssgCacheKey ?? (opts.dev && isAppPath ? resolvedUrlPathname : null) + if (staticPathKey && query.amp) { + staticPathKey = staticPathKey.replace(/\.amp$/, '') + } + + const isPageIncludedInStaticPaths = + staticPathKey && staticPaths?.includes(staticPathKey) + // When we did not respond from cache, we need to choose to block on // rendering or return a skeleton. // - // * Data requests always block. - // - // * Blocking mode fallback always blocks. - // - // * Preview mode toggles all pages to be resolved in a blocking manner. - // - // * Non-dynamic pages should block (though this is an impossible + // - Data requests always block. + // - Blocking mode fallback always blocks. + // - Preview mode toggles all pages to be resolved in a blocking manner. + // - Non-dynamic pages should block (though this is an impossible // case in production). - // - // * Dynamic pages should return their skeleton if not defined in + // - Dynamic pages should return their skeleton if not defined in // getStaticPaths, then finish the data request on the client-side. // if ( process.env.NEXT_RUNTIME !== 'edge' && - this.minimalMode !== true && + !this.minimalMode && fallbackMode !== 'blocking' && - ssgCacheKey && + staticPathKey && !didRespond && !isPreviewMode && isDynamicPathname && - // Development should trigger fallback when the path is not in - // `getStaticPaths` - (isProduction || - !staticPaths || - !staticPaths.includes( - // we use ssgCacheKey here as it is normalized to match the - // encoding from getStaticPaths along with including the locale - query.amp ? ssgCacheKey.replace(/\.amp$/, '') : ssgCacheKey - )) + (isProduction || !staticPaths || !isPageIncludedInStaticPaths) ) { if ( // In development, fall through to render to handle missing // getStaticPaths. - (isProduction || staticPaths) && + (isProduction || (staticPaths && staticPaths?.length > 0)) && // When fallback isn't present, abort this render so we 404 fallbackMode !== 'static' ) { diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index c0e506198d175..39177d3c143d9 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -163,6 +163,7 @@ createNextDescribe( 'dynamic-no-gen-params-ssr/[slug]/page.js', 'dynamic-no-gen-params/[slug]/page.js', 'force-dynamic-no-prerender/[id]/page.js', + 'force-dynamic-prerender/[slug]/page.js', 'force-static/[slug]/page.js', 'force-static/first.html', 'force-static/first.rsc', @@ -1148,6 +1149,26 @@ createNextDescribe( } }) + it('should allow dynamic routes to access cookies', async () => { + for (const slug of ['books', 'frameworks']) { + for (let i = 0; i < 2; i++) { + let $ = await next.render$( + `/force-dynamic-prerender/${slug}`, + {}, + { headers: { cookie: 'session=value' } } + ) + + expect($('#slug').text()).toBe(slug) + expect($('#cookie-result').text()).toBe('has cookie') + + $ = await next.render$(`/force-dynamic-prerender/${slug}`) + + expect($('#slug').text()).toBe(slug) + expect($('#cookie-result').text()).toBe('no cookie') + } + } + }) + it('should not error with generateStaticParams and dynamic data', async () => { const res = await next.fetch('/gen-params-dynamic/one') const html = await res.text() diff --git a/test/e2e/app-dir/app-static/app/force-dynamic-prerender/[slug]/page.js b/test/e2e/app-dir/app-static/app/force-dynamic-prerender/[slug]/page.js new file mode 100644 index 0000000000000..829fbcb150e2e --- /dev/null +++ b/test/e2e/app-dir/app-static/app/force-dynamic-prerender/[slug]/page.js @@ -0,0 +1,19 @@ +import { cookies } from 'next/headers' + +export const dynamic = 'force-dynamic' +export const dynamicParams = true +export const revalidate = 60 + +export const generateStaticParams = async () => { + return [{ slug: 'frameworks' }] +} + +export default function Page({ params }) { + const result = cookies().get('session')?.value ? 'has cookie' : 'no cookie' + return ( +
+
{params.slug}
+ +
+ ) +}