diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts
index db761f6871d3e..28c2db77b797a 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts
@@ -116,6 +116,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
previewProps: encodedPreviewProps,
env: process.env,
basePath,
+ requireStaticHTML: true, // Serverless target doesn't support streaming
..._renderOpts,
}
let _nextData = false
diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx
index 762a18b7c1c0a..2ae9a764e4574 100644
--- a/packages/next/server/render.tsx
+++ b/packages/next/server/render.tsx
@@ -3,6 +3,7 @@ import { ParsedUrlQuery } from 'querystring'
import { PassThrough } from 'stream'
import React from 'react'
import * as ReactDOMServer from 'react-dom/server'
+import flush from 'styled-jsx/server'
import Observable from 'next/dist/compiled/zen-observable'
import { warn } from '../build/output/log'
import { UnwrapPromise } from '../lib/coalesced-function'
@@ -39,8 +40,8 @@ import {
ComponentsEnhancer,
DocumentInitialProps,
DocumentProps,
- DocumentType,
HtmlContext,
+ HtmlProps,
getDisplayName,
isResSent,
loadGetInitialProps,
@@ -195,131 +196,11 @@ export type RenderOptsPartial = {
disableOptimizedLoading?: boolean
requireStaticHTML?: boolean
concurrentFeatures?: boolean
+ customServer?: boolean
}
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
-function renderDocument(
- Document: DocumentType,
- {
- buildManifest,
- docComponentsRendered,
- props,
- docProps,
- pathname,
- query,
- buildId,
- canonicalBase,
- assetPrefix,
- runtimeConfig,
- nextExport,
- autoExport,
- isFallback,
- dynamicImportsIds,
- dangerousAsPath,
- err,
- dev,
- ampPath,
- ampState,
- inAmpMode,
- hybridAmp,
- dynamicImports,
- headTags,
- gsp,
- gssp,
- customServer,
- gip,
- appGip,
- unstable_runtimeJS,
- unstable_JsPreload,
- devOnlyCacheBusterQueryString,
- scriptLoader,
- locale,
- locales,
- defaultLocale,
- domainLocales,
- isPreview,
- disableOptimizedLoading,
- }: RenderOpts & {
- props: any
- docComponentsRendered: DocumentProps['docComponentsRendered']
- docProps: DocumentInitialProps
- pathname: string
- query: ParsedUrlQuery
- dangerousAsPath: string
- ampState: any
- ampPath: string
- inAmpMode: boolean
- hybridAmp: boolean
- dynamicImportsIds: (string | number)[]
- dynamicImports: string[]
- headTags: any
- isFallback?: boolean
- gsp?: boolean
- gssp?: boolean
- customServer?: boolean
- gip?: boolean
- appGip?: boolean
- devOnlyCacheBusterQueryString: string
- scriptLoader: any
- isPreview?: boolean
- autoExport?: boolean
- }
-): string {
- const htmlProps = {
- __NEXT_DATA__: {
- props, // The result of getInitialProps
- page: pathname, // The rendered page
- query, // querystring parsed / passed by the user
- buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
- assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
- runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
- nextExport, // If this is a page exported by `next export`
- autoExport, // If this is an auto exported page
- isFallback,
- dynamicIds:
- dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
- err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
- gsp, // whether the page is getStaticProps
- gssp, // whether the page is getServerSideProps
- customServer, // whether the user is using a custom server
- gip, // whether the page has getInitialProps
- appGip, // whether the _app has getInitialProps
- locale,
- locales,
- defaultLocale,
- domainLocales,
- isPreview,
- },
- buildManifest,
- docComponentsRendered,
- dangerousAsPath,
- canonicalBase,
- ampPath,
- inAmpMode,
- isDevelopment: !!dev,
- hybridAmp,
- dynamicImports,
- assetPrefix,
- headTags,
- unstable_runtimeJS,
- unstable_JsPreload,
- devOnlyCacheBusterQueryString,
- scriptLoader,
- locale,
- disableOptimizedLoading,
- styles: docProps.styles,
- head: docProps.head,
- }
- return ReactDOMServer.renderToStaticMarkup(
-
-
-
-
-
- )
-}
-
const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
return (
`Additional keys were returned from \`${methodName}\`. Properties intended for your component must be nested under the \`props\` key, e.g.:` +
@@ -637,7 +518,7 @@ export async function renderToHTML(
const nextExport =
!isSSG && (renderOpts.nextExport || (dev && (isAutoExport || isFallback)))
- const AppContainer = ({ children }: any) => (
+ const AppContainer = ({ children }: { children: JSX.Element }) => (
{
- const result = await renderToStream(element)
- return await resultsToString([result])
- }
- : ReactDOMServer.renderToString
-
- const renderPage: RenderPage = (
- options: ComponentsEnhancer = {}
- ): RenderPageResult | Promise => {
- if (ctx.err && ErrorDebug) {
- const htmlOrPromise = renderToString()
- return typeof htmlOrPromise === 'string'
- ? { html: htmlOrPromise, head }
- : htmlOrPromise.then((html) => ({
- html,
- head,
- }))
- }
+ const renderDocument = async () => {
+ if (Document.getInitialProps) {
+ const renderPage: RenderPage = (
+ options: ComponentsEnhancer = {}
+ ): RenderPageResult | Promise => {
+ if (ctx.err && ErrorDebug) {
+ const html = ReactDOMServer.renderToString(
+
+ )
+ return { html, head }
+ }
- if (dev && (props.router || props.Component)) {
- throw new Error(
- `'router' and 'Component' can not be returned in getInitialProps from _app.js https://nextjs.org/docs/messages/cant-override-next-props`
- )
- }
+ if (dev && (props.router || props.Component)) {
+ throw new Error(
+ `'router' and 'Component' can not be returned in getInitialProps from _app.js https://nextjs.org/docs/messages/cant-override-next-props`
+ )
+ }
- const { App: EnhancedApp, Component: EnhancedComponent } =
- enhanceComponents(options, App, Component)
+ const { App: EnhancedApp, Component: EnhancedComponent } =
+ enhanceComponents(options, App, Component)
+
+ const html = ReactDOMServer.renderToString(
+
+
+
+ )
+ return { html, head }
+ }
+ const documentCtx = { ...ctx, renderPage }
+ const docProps: DocumentInitialProps = await loadGetInitialProps(
+ Document,
+ documentCtx
+ )
+ // the response might be finished on the getInitialProps call
+ if (isResSent(res) && !isSSG) return null
+
+ if (!docProps || typeof docProps.html !== 'string') {
+ const message = `"${getDisplayName(
+ Document
+ )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
+ throw new Error(message)
+ }
- const htmlOrPromise = renderToString(
-
-
-
- )
- return typeof htmlOrPromise === 'string'
- ? { html: htmlOrPromise, head }
- : htmlOrPromise.then((html) => ({
- html,
- head,
- }))
+ return {
+ bodyResult: Observable.of(docProps.html),
+ documentElement: (htmlProps: HtmlProps) => (
+
+ ),
+ head: docProps.head,
+ headTags: await headTags(documentCtx),
+ styles: docProps.styles,
+ }
+ } else {
+ const content =
+ ctx.err && ErrorDebug ? (
+
+ ) : (
+
+
+
+ )
+ const bodyResult = concurrentFeatures
+ ? await renderToStream(content)
+ : Observable.of(ReactDOMServer.renderToString(content))
+
+ return {
+ bodyResult,
+ documentElement: () => (Document as any)(),
+ head,
+ headTags: [],
+ // TODO: Experimental styled-jsx 5 support
+ styles: [...flush()],
+ }
+ }
}
- const documentCtx = { ...ctx, renderPage }
- const docProps: DocumentInitialProps = await loadGetInitialProps(
- Document,
- documentCtx
- )
- // the response might be finished on the getInitialProps call
- if (isResSent(res) && !isSSG) return null
- if (!docProps || typeof docProps.html !== 'string') {
- const message = `"${getDisplayName(
- Document
- )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
- throw new Error(message)
+ const documentResult = await renderDocument()
+ if (!documentResult) {
+ return null
}
const dynamicImportsIds = new Set()
@@ -1122,44 +1032,78 @@ export async function renderToHTML(
const hybridAmp = ampState.hybrid
const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}
-
- const documentHTML = renderDocument(Document, {
- ...renderOpts,
+ const {
+ assetPrefix,
+ buildId,
+ customServer,
+ defaultLocale,
+ disableOptimizedLoading,
+ domainLocales,
+ locale,
+ locales,
+ runtimeConfig,
+ } = renderOpts
+ const htmlProps: any = {
+ __NEXT_DATA__: {
+ props, // The result of getInitialProps
+ page: pathname, // The rendered page
+ query, // querystring parsed / passed by the user
+ buildId, // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
+ assetPrefix: assetPrefix === '' ? undefined : assetPrefix, // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
+ runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML
+ nextExport: nextExport === true ? true : undefined, // If this is a page exported by `next export`
+ autoExport: isAutoExport === true ? true : undefined, // If this is an auto exported page
+ isFallback,
+ dynamicIds:
+ dynamicImportsIds.size === 0
+ ? undefined
+ : Array.from(dynamicImportsIds),
+ err: renderOpts.err ? serializeError(dev, renderOpts.err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
+ gsp: !!getStaticProps ? true : undefined, // whether the page is getStaticProps
+ gssp: !!getServerSideProps ? true : undefined, // whether the page is getServerSideProps
+ customServer, // whether the user is using a custom server
+ gip: hasPageGetInitialProps ? true : undefined, // whether the page has getInitialProps
+ appGip: !defaultAppGetInitialProps ? true : undefined, // whether the _app has getInitialProps
+ locale,
+ locales,
+ defaultLocale,
+ domainLocales,
+ isPreview: isPreview === true ? true : undefined,
+ },
+ buildManifest: filteredBuildManifest,
+ docComponentsRendered,
+ dangerousAsPath: router.asPath,
canonicalBase:
!renderOpts.ampPath && (req as any).__nextStrippedLocale
? `${renderOpts.canonicalBase || ''}/${renderOpts.locale}`
: renderOpts.canonicalBase,
- docComponentsRendered,
- buildManifest: filteredBuildManifest,
+ ampPath,
+ inAmpMode,
+ isDevelopment: !!dev,
+ hybridAmp,
+ dynamicImports: Array.from(dynamicImports),
+ assetPrefix,
// Only enabled in production as development mode has features relying on HMR (style injection for example)
unstable_runtimeJS:
process.env.NODE_ENV === 'production'
? pageConfig.unstable_runtimeJS
: undefined,
unstable_JsPreload: pageConfig.unstable_JsPreload,
- dangerousAsPath: router.asPath,
- ampState,
- props,
- headTags: await headTags(documentCtx),
- isFallback,
- docProps,
- pathname,
- ampPath,
- query,
- inAmpMode,
- hybridAmp,
- dynamicImportsIds: Array.from(dynamicImportsIds),
- dynamicImports: Array.from(dynamicImports),
- gsp: !!getStaticProps ? true : undefined,
- gssp: !!getServerSideProps ? true : undefined,
- gip: hasPageGetInitialProps ? true : undefined,
- appGip: !defaultAppGetInitialProps ? true : undefined,
devOnlyCacheBusterQueryString,
scriptLoader,
- isPreview: isPreview === true ? true : undefined,
- autoExport: isAutoExport === true ? true : undefined,
- nextExport: nextExport === true ? true : undefined,
- })
+ locale,
+ disableOptimizedLoading,
+ head: documentResult.head,
+ headTags: documentResult?.headTags,
+ styles: documentResult.styles,
+ }
+ const documentHTML = ReactDOMServer.renderToStaticMarkup(
+
+
+ {documentResult.documentElement(htmlProps)}
+
+
+ )
if (process.env.NODE_ENV !== 'production') {
const nonRenderedComponents = []
@@ -1194,57 +1138,62 @@ export async function renderToHTML(
if (inAmpMode) {
results.push(Observable.of(''))
}
- results.push(Observable.of(docProps.html))
+ results.push(documentResult.bodyResult)
results.push(
Observable.of(
documentHTML.substring(renderTargetIdx + BODY_RENDER_TARGET.length)
)
)
- const postProcessors: Array<((html: string) => Promise) | null> = [
- inAmpMode
- ? async (html: string) => {
- html = await optimizeAmp(html, renderOpts.ampOptimizerConfig)
- if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
- await renderOpts.ampValidator(html, pathname)
- }
- return html
- }
- : null,
- process.env.__NEXT_OPTIMIZE_FONTS || process.env.__NEXT_OPTIMIZE_IMAGES
- ? async (html: string) => {
- return await postProcess(
- html,
- { getFontDefinition },
- {
- optimizeFonts: renderOpts.optimizeFonts,
- optimizeImages: renderOpts.optimizeImages,
- }
- )
- }
- : null,
- renderOpts.optimizeCss
- ? async (html: string) => {
- // eslint-disable-next-line import/no-extraneous-dependencies
- const Critters = require('critters')
- const cssOptimizer = new Critters({
- ssrMode: true,
- reduceInlineStyles: false,
- path: renderOpts.distDir,
- publicPath: `${renderOpts.assetPrefix}/_next/`,
- preload: 'media',
- fonts: false,
- ...renderOpts.optimizeCss,
- })
- return await cssOptimizer.process(html)
- }
- : null,
- inAmpMode || hybridAmp
- ? async (html: string) => {
- return html.replace(/&=1/g, '&=1')
- }
- : null,
- ].filter(Boolean)
+ const postProcessors: Array<((html: string) => Promise) | null> = (
+ generateStaticHTML
+ ? [
+ inAmpMode
+ ? async (html: string) => {
+ html = await optimizeAmp(html, renderOpts.ampOptimizerConfig)
+ if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
+ await renderOpts.ampValidator(html, pathname)
+ }
+ return html
+ }
+ : null,
+ process.env.__NEXT_OPTIMIZE_FONTS ||
+ process.env.__NEXT_OPTIMIZE_IMAGES
+ ? async (html: string) => {
+ return await postProcess(
+ html,
+ { getFontDefinition },
+ {
+ optimizeFonts: renderOpts.optimizeFonts,
+ optimizeImages: renderOpts.optimizeImages,
+ }
+ )
+ }
+ : null,
+ renderOpts.optimizeCss
+ ? async (html: string) => {
+ // eslint-disable-next-line import/no-extraneous-dependencies
+ const Critters = require('critters')
+ const cssOptimizer = new Critters({
+ ssrMode: true,
+ reduceInlineStyles: false,
+ path: renderOpts.distDir,
+ publicPath: `${renderOpts.assetPrefix}/_next/`,
+ preload: 'media',
+ fonts: false,
+ ...renderOpts.optimizeCss,
+ })
+ return await cssOptimizer.process(html)
+ }
+ : null,
+ inAmpMode || hybridAmp
+ ? async (html: string) => {
+ return html.replace(/&=1/g, '&=1')
+ }
+ : null,
+ ]
+ : []
+ ).filter(Boolean)
if (postProcessors.length > 0) {
let html = await resultsToString(results)
diff --git a/test/integration/react-18/app/pages/_document.js b/test/integration/react-18/app/pages/_document.js
new file mode 100644
index 0000000000000..bff2b1b2821cb
--- /dev/null
+++ b/test/integration/react-18/app/pages/_document.js
@@ -0,0 +1,13 @@
+import { Html, Head, Main, NextScript } from 'next/document'
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+ )
+}