import { IncomingMessage, ServerResponse } from 'http' import { ParsedUrlQuery } from 'querystring' import React from 'react' import { renderToStaticMarkup, renderToString } from 'react-dom/server' import { PAGES_404_GET_INITIAL_PROPS_ERROR, SERVER_PROPS_GET_INIT_PROPS_CONFLICT, SERVER_PROPS_SSG_CONFLICT, SSG_GET_INITIAL_PROPS_CONFLICT, } from '../../lib/constants' import { isSerializableProps } from '../../lib/is-serializable-props' import { isInAmpMode } from '../lib/amp' import { AmpStateContext } from '../lib/amp-context' import { AMP_RENDER_TARGET, STATIC_PROPS_ID, SERVER_PROPS_ID, } from '../lib/constants' import Head, { defaultHead } from '../lib/head' import Loadable from '../lib/loadable' import { LoadableContext } from '../lib/loadable-context' import mitt, { MittEmitter } from '../lib/mitt' import { RouterContext } from '../lib/router-context' import { NextRouter } from '../lib/router/router' import { isDynamicRoute } from '../lib/router/utils/is-dynamic' import { AppType, ComponentsEnhancer, DocumentInitialProps, DocumentType, getDisplayName, isResSent, loadGetInitialProps, NextComponentType, RenderPage, } from '../lib/utils' import { tryGetPreviewData, __ApiPreviewProps } from './api-utils' import { getPageFiles } from './get-page-files' import { LoadComponentsReturnType, ManifestItem } from './load-components' import optimizeAmp from './optimize-amp' function noRouter() { const message = 'No router instance found. you should only use "next/router" inside the client side of your app. https://err.sh/zeit/next.js/no-router-instance' throw new Error(message) } class ServerRouter implements NextRouter { route: string pathname: string query: ParsedUrlQuery asPath: string events: any isFallback: boolean // TODO: Remove in the next major version, as this would mean the user is adding event listeners in server-side `render` method static events: MittEmitter = mitt() constructor( pathname: string, query: ParsedUrlQuery, as: string, { isFallback }: { isFallback: boolean } ) { this.route = pathname.replace(/\/$/, '') || '/' this.pathname = pathname this.query = query this.asPath = as this.isFallback = isFallback } push(): any { noRouter() } replace(): any { noRouter() } reload() { noRouter() } back() { noRouter() } prefetch(): any { noRouter() } beforePopState() { noRouter() } } function enhanceComponents( options: ComponentsEnhancer, App: AppType, Component: NextComponentType ): { App: AppType Component: NextComponentType } { // For backwards compatibility if (typeof options === 'function') { return { App, Component: options(Component), } } return { App: options.enhanceApp ? options.enhanceApp(App) : App, Component: options.enhanceComponent ? options.enhanceComponent(Component) : Component, } } function render( renderElementToString: (element: React.ReactElement) => string, element: React.ReactElement, ampMode: any ): { html: string; head: React.ReactElement[] } { let html let head try { html = renderElementToString(element) } finally { head = Head.rewind() || defaultHead(isInAmpMode(ampMode)) } return { html, head } } export type RenderOptsPartial = { staticMarkup: boolean buildId: string canonicalBase: string runtimeConfig?: { [key: string]: any } assetPrefix?: string hasCssMode: boolean err?: Error | null autoExport?: boolean nextExport?: boolean dev?: boolean ampMode?: any ampPath?: string inAmpMode?: boolean hybridAmp?: boolean ErrorDebug?: React.ComponentType<{ error: Error }> ampValidator?: (html: string, pathname: string) => Promise documentMiddlewareEnabled?: boolean isDataReq?: boolean params?: ParsedUrlQuery previewProps: __ApiPreviewProps } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial function renderDocument( Document: DocumentType, { props, docProps, pathname, query, buildId, canonicalBase, assetPrefix, runtimeConfig, nextExport, autoExport, isFallback, dynamicImportsIds, dangerousAsPath, hasCssMode, err, dev, ampPath, ampState, inAmpMode, hybridAmp, staticMarkup, devFiles, files, lowPriorityFiles, polyfillFiles, dynamicImports, htmlProps, bodyTags, headTags, gsp, gssp, customServer, }: RenderOpts & { props: any docProps: DocumentInitialProps pathname: string query: ParsedUrlQuery dangerousAsPath: string ampState: any ampPath: string inAmpMode: boolean hybridAmp: boolean dynamicImportsIds: string[] dynamicImports: ManifestItem[] devFiles: string[] files: string[] lowPriorityFiles: string[] polyfillFiles: string[] htmlProps: any bodyTags: any headTags: any isFallback?: boolean gsp?: boolean gssp?: boolean customServer?: boolean } ): string { return ( '' + renderToStaticMarkup( {Document.renderDocument(Document, { __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 }, dangerousAsPath, canonicalBase, ampPath, inAmpMode, isDevelopment: !!dev, hasCssMode, hybridAmp, staticMarkup, devFiles, files, lowPriorityFiles, polyfillFiles, dynamicImports, assetPrefix, htmlProps, bodyTags, headTags, ...docProps, })} ) ) } 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.:` + `\n\n\treturn { props: { title: 'My Title', content: '...' } }` + `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.` + `\nRead more: https://err.sh/next.js/invalid-getstaticprops-value` ) } export async function renderToHTML( req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery, renderOpts: RenderOpts ): Promise { pathname = pathname === '/index' ? '/' : pathname const { err, dev = false, documentMiddlewareEnabled = false, staticMarkup = false, ampPath = '', App, Document, pageConfig = {}, DocumentMiddleware, Component, buildManifest, reactLoadableManifest, ErrorDebug, getStaticProps, getStaticPaths, getServerSideProps, isDataReq, params, previewProps, } = renderOpts const callMiddleware = async (method: string, args: any[], props = false) => { let results: any = props ? {} : [] if ((Document as any)[`${method}Middleware`]) { let middlewareFunc = await (Document as any)[`${method}Middleware`] middlewareFunc = middlewareFunc.default || middlewareFunc const curResults = await middlewareFunc(...args) if (props) { for (const result of curResults) { results = { ...results, ...result, } } } else { results = curResults } } return results } const headTags = (...args: any) => callMiddleware('headTags', args) const bodyTags = (...args: any) => callMiddleware('bodyTags', args) const htmlProps = (...args: any) => callMiddleware('htmlProps', args, true) const didRewrite = (req as any)._nextDidRewrite const isFallback = !!query.__nextFallback delete query.__nextFallback const isSSG = !!getStaticProps const isBuildTimeSSG = isSSG && renderOpts.nextExport const defaultAppGetInitialProps = App.getInitialProps === (App as any).origGetInitialProps const hasPageGetInitialProps = !!(Component as any).getInitialProps const pageIsDynamic = isDynamicRoute(pathname) const isAutoExport = !hasPageGetInitialProps && defaultAppGetInitialProps && !isSSG && !getServerSideProps if ( process.env.NODE_ENV !== 'production' && (isAutoExport || isFallback) && pageIsDynamic && didRewrite ) { // TODO: add err.sh when rewrites go stable // Behavior might change before then (prefer SSR in this case). // If we decide to ship rewrites to the client we could solve this // by running over the rewrites and getting the params. throw new Error( `Rewrites don't support${ isFallback ? ' ' : ' auto-exported ' }dynamic pages${isFallback ? ' with getStaticProps ' : ' '}yet. ` + `Using this will cause the page to fail to parse the params on the client${ isFallback ? ' for the fallback page ' : '' }` ) } if (hasPageGetInitialProps && isSSG) { throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`) } if (hasPageGetInitialProps && getServerSideProps) { throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT + ` ${pathname}`) } if (getServerSideProps && isSSG) { throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`) } if (!!getStaticPaths && !isSSG) { throw new Error( `getStaticPaths was added without a getStaticProps in ${pathname}. Without getStaticProps, getStaticPaths does nothing` ) } if (isSSG && pageIsDynamic && !getStaticPaths) { throw new Error( `getStaticPaths is required for dynamic SSG pages and is missing for '${pathname}'.` + `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value` ) } if (dev) { const { isValidElementType } = require('react-is') if (!isValidElementType(Component)) { throw new Error( `The default export is not a React Component in page: "${pathname}"` ) } if (!isValidElementType(App)) { throw new Error( `The default export is not a React Component in page: "/_app"` ) } if (!isValidElementType(Document)) { throw new Error( `The default export is not a React Component in page: "/_document"` ) } if (isAutoExport) { // remove query values except ones that will be set during export query = { amp: query.amp, } req.url = pathname renderOpts.nextExport = true } if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) { throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR) } } if (isAutoExport) renderOpts.autoExport = true if (isSSG) renderOpts.nextExport = false await Loadable.preloadAll() // Make sure all dynamic imports are loaded // url will always be set const asPath = req.url as string const router = new ServerRouter(pathname, query, asPath, { isFallback: isFallback, }) const ctx = { err, req: isAutoExport ? undefined : req, res: isAutoExport ? undefined : res, pathname, query, asPath, AppTree: (props: any) => { return ( ) }, } let props: any if (documentMiddlewareEnabled && typeof DocumentMiddleware === 'function') { await DocumentMiddleware(ctx) } const ampState = { ampFirst: pageConfig.amp === true, hasQuery: Boolean(query.amp), hybrid: pageConfig.amp === 'hybrid', } const reactLoadableModules: string[] = [] const AppContainer = ({ children }: any) => ( reactLoadableModules.push(moduleName)} > {children} ) try { props = await loadGetInitialProps(App, { AppTree: ctx.AppTree, Component, router, ctx, }) if (isSSG) { props[STATIC_PROPS_ID] = true } let previewData: string | false | object | undefined if ((isSSG || getServerSideProps) && !isFallback) { // Reads of this are cached on the `req` object, so this should resolve // instantly. There's no need to pass this data down from a previous // invoke, where we'd have to consider server & serverless. previewData = tryGetPreviewData(req, res, previewProps) } if (isSSG && !isFallback) { const data = await getStaticProps!({ ...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined), ...(previewData !== false ? { preview: true, previewData: previewData } : undefined), }) const invalidKeys = Object.keys(data).filter( key => key !== 'revalidate' && key !== 'props' ) if (invalidKeys.length) { throw new Error(invalidKeysMsg('getStaticProps', invalidKeys)) } if ( (dev || isBuildTimeSSG) && !isSerializableProps(pathname, 'getStaticProps', data.props) ) { // this fn should throw an error instead of ever returning `false` throw new Error( 'invariant: getStaticProps did not return valid props. Please report this.' ) } if (typeof data.revalidate === 'number') { if (!Number.isInteger(data.revalidate)) { throw new Error( `A page's revalidate option must be seconds expressed as a natural number. Mixed numbers, such as '${data.revalidate}', cannot be used.` + `\nTry changing the value to '${Math.ceil( data.revalidate )}' or using \`Math.ceil()\` if you're computing the value.` ) } else if (data.revalidate <= 0) { throw new Error( `A page's revalidate option can not be less than or equal to zero. A revalidate option of zero means to revalidate after _every_ request, and implies stale data cannot be tolerated.` + `\n\nTo never revalidate, you can set revalidate to \`false\` (only ran once at build-time).` + `\nTo revalidate as soon as possible, you can set the value to \`1\`.` ) } else if (data.revalidate > 31536000) { // if it's greater than a year for some reason error console.warn( `Warning: A page's revalidate option was set to more than a year. This may have been done in error.` + `\nTo only run getStaticProps at build-time and not revalidate at runtime, you can set \`revalidate\` to \`false\`!` ) } } else if (data.revalidate === true) { // When enabled, revalidate after 1 second. This value is optimal for // the most up-to-date page possible, but without a 1-to-1 // request-refresh ratio. data.revalidate = 1 } else { // By default, we never revalidate. data.revalidate = false } props.pageProps = data.props // pass up revalidate and props for export // TODO: change this to a different passing mechanism ;(renderOpts as any).revalidate = data.revalidate ;(renderOpts as any).pageData = props } if (getServerSideProps) { props[SERVER_PROPS_ID] = true } if (getServerSideProps && !isFallback) { const data = await getServerSideProps({ req, res, query, ...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined), ...(previewData !== false ? { preview: true, previewData: previewData } : undefined), }) const invalidKeys = Object.keys(data).filter(key => key !== 'props') if (invalidKeys.length) { throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys)) } if ( (dev || isBuildTimeSSG) && !isSerializableProps(pathname, 'getServerSideProps', data.props) ) { // this fn should throw an error instead of ever returning `false` throw new Error( 'invariant: getServerSideProps did not return valid props. Please report this.' ) } props.pageProps = data.props ;(renderOpts as any).pageData = props } } catch (err) { if (isDataReq || !dev || !err) throw err ctx.err = err renderOpts.err = err console.error(err) } if ( !isSSG && // we only show this warning for legacy pages !getServerSideProps && process.env.NODE_ENV !== 'production' && Object.keys(props?.pageProps || {}).includes('url') ) { console.warn( `The prop \`url\` is a reserved prop in Next.js for legacy reasons and will be overridden on page ${pathname}\n` + `See more info here: https://err.sh/zeit/next.js/reserved-page-prop` ) } // We only need to do this if we want to support calling // _app's getInitialProps for getServerSideProps if not this can be removed if (isDataReq) return props // We don't call getStaticProps or getServerSideProps while generating // the fallback so make sure to set pageProps to an empty object if (isFallback) { props.pageProps = {} } // the response might be finished on the getInitialProps call if (isResSent(res) && !isSSG) return null const devFiles = buildManifest.devFiles const files = [ ...new Set([ ...getPageFiles(buildManifest, '/_app'), ...getPageFiles(buildManifest, pathname), ]), ] const lowPriorityFiles = buildManifest.lowPriorityFiles const polyfillFiles = getPageFiles(buildManifest, '/_polyfills') const renderElementToString = staticMarkup ? renderToStaticMarkup : renderToString const renderPageError = (): { html: string; head: any } | void => { if (ctx.err && ErrorDebug) { return render( renderElementToString, , ampState ) } if (dev && (props.router || props.Component)) { throw new Error( `'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/zeit/next.js/cant-override-next-props` ) } } let renderPage: RenderPage = ( options: ComponentsEnhancer = {} ): { html: string; head: any } => { const renderError = renderPageError() if (renderError) return renderError const { App: EnhancedApp, Component: EnhancedComponent, } = enhanceComponents(options, App, Component) return render( renderElementToString, , ampState ) } 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 dynamicImportIdsSet = new Set() const dynamicImports: ManifestItem[] = [] for (const mod of reactLoadableModules) { const manifestItem: ManifestItem[] = reactLoadableManifest[mod] if (manifestItem) { manifestItem.forEach(item => { dynamicImports.push(item) dynamicImportIdsSet.add(item.id as string) }) } } const dynamicImportsIds = [...dynamicImportIdsSet] const inAmpMode = isInAmpMode(ampState) const hybridAmp = ampState.hybrid // update renderOpts so export knows current state renderOpts.inAmpMode = inAmpMode renderOpts.hybridAmp = hybridAmp let html = renderDocument(Document, { ...renderOpts, dangerousAsPath: router.asPath, ampState, props, headTags: await headTags(documentCtx), bodyTags: await bodyTags(documentCtx), htmlProps: await htmlProps(documentCtx), isFallback, docProps, pathname, ampPath, query, inAmpMode, hybridAmp, dynamicImportsIds, dynamicImports, devFiles, files, lowPriorityFiles, polyfillFiles, gsp: !!getStaticProps ? true : undefined, gssp: !!getServerSideProps ? true : undefined, }) if (inAmpMode && html) { // inject HTML to AMP_RENDER_TARGET to allow rendering // directly to body in AMP mode const ampRenderIndex = html.indexOf(AMP_RENDER_TARGET) html = html.substring(0, ampRenderIndex) + `${docProps.html}` + html.substring(ampRenderIndex + AMP_RENDER_TARGET.length) html = await optimizeAmp(html) if (renderOpts.ampValidator) { await renderOpts.ampValidator(html, pathname) } } if (inAmpMode || hybridAmp) { // fix & being escaped for amphtml rel link html = html.replace(/&amp=1/g, '&=1') } return html } function errorToJSON(err: Error): Error { const { name, message, stack } = err return { name, message, stack } } function serializeError( dev: boolean | undefined, err: Error ): Error & { statusCode?: number } { if (dev) { return errorToJSON(err) } return { name: 'Internal Server Error.', message: '500 - Internal Server Error.', statusCode: 500, } }