4ef1cd4ebe
* Add identifier to NEXT_DATA for gs(s)p * Apply suggestions from code review Co-Authored-By: Joe Haddad <joe.haddad@zeit.co> * fix lint * apply lint fix Co-authored-by: Joe Haddad <timer150@gmail.com>
734 lines
21 KiB
TypeScript
734 lines
21 KiB
TypeScript
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 { isInAmpMode } from '../lib/amp'
|
|
import { AmpStateContext } from '../lib/amp-context'
|
|
import { AMP_RENDER_TARGET } 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<any>) => string,
|
|
element: React.ReactElement<any>,
|
|
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<void>
|
|
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,
|
|
}: 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
|
|
}
|
|
): string {
|
|
return (
|
|
'<!DOCTYPE html>' +
|
|
renderToStaticMarkup(
|
|
<AmpStateContext.Provider value={ampState}>
|
|
{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
|
|
},
|
|
dangerousAsPath,
|
|
canonicalBase,
|
|
ampPath,
|
|
inAmpMode,
|
|
isDevelopment: !!dev,
|
|
hasCssMode,
|
|
hybridAmp,
|
|
staticMarkup,
|
|
devFiles,
|
|
files,
|
|
lowPriorityFiles,
|
|
polyfillFiles,
|
|
dynamicImports,
|
|
assetPrefix,
|
|
htmlProps,
|
|
bodyTags,
|
|
headTags,
|
|
...docProps,
|
|
})}
|
|
</AmpStateContext.Provider>
|
|
)
|
|
)
|
|
}
|
|
|
|
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<string | null> {
|
|
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 isSpr = !!getStaticProps
|
|
const defaultAppGetInitialProps =
|
|
App.getInitialProps === (App as any).origGetInitialProps
|
|
|
|
const hasPageGetInitialProps = !!(Component as any).getInitialProps
|
|
|
|
const pageIsDynamic = isDynamicRoute(pathname)
|
|
|
|
const isAutoExport =
|
|
!hasPageGetInitialProps &&
|
|
defaultAppGetInitialProps &&
|
|
!isSpr &&
|
|
!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 && isSpr) {
|
|
throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
|
|
}
|
|
|
|
if (hasPageGetInitialProps && getServerSideProps) {
|
|
throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT + ` ${pathname}`)
|
|
}
|
|
|
|
if (getServerSideProps && isSpr) {
|
|
throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`)
|
|
}
|
|
|
|
if (!!getStaticPaths && !isSpr) {
|
|
throw new Error(
|
|
`getStaticPaths was added without a getStaticProps in ${pathname}. Without getStaticProps, getStaticPaths does nothing`
|
|
)
|
|
}
|
|
|
|
if (isSpr && 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' && !isAutoExport) {
|
|
throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR)
|
|
}
|
|
}
|
|
if (isAutoExport) renderOpts.autoExport = true
|
|
if (isSpr) 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 (
|
|
<AppContainer>
|
|
<App {...props} Component={Component} router={router} />
|
|
</AppContainer>
|
|
)
|
|
},
|
|
}
|
|
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) => (
|
|
<RouterContext.Provider value={router}>
|
|
<AmpStateContext.Provider value={ampState}>
|
|
<LoadableContext.Provider
|
|
value={moduleName => reactLoadableModules.push(moduleName)}
|
|
>
|
|
{children}
|
|
</LoadableContext.Provider>
|
|
</AmpStateContext.Provider>
|
|
</RouterContext.Provider>
|
|
)
|
|
|
|
try {
|
|
props = await loadGetInitialProps(App, {
|
|
AppTree: ctx.AppTree,
|
|
Component,
|
|
router,
|
|
ctx,
|
|
})
|
|
|
|
if (isSpr && !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.
|
|
const previewData = tryGetPreviewData(req, res, previewProps)
|
|
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 (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
|
|
}
|
|
} catch (err) {
|
|
if (!dev || !err) throw err
|
|
ctx.err = err
|
|
renderOpts.err = err
|
|
console.error(err)
|
|
}
|
|
|
|
if (getServerSideProps && !isFallback) {
|
|
const data = await getServerSideProps({
|
|
req,
|
|
res,
|
|
...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined),
|
|
query,
|
|
})
|
|
|
|
const invalidKeys = Object.keys(data).filter(key => key !== 'props')
|
|
|
|
if (invalidKeys.length) {
|
|
throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
|
|
}
|
|
|
|
props.pageProps = data.props
|
|
;(renderOpts as any).pageData = props
|
|
}
|
|
|
|
if (
|
|
!isSpr && // 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) && !isSpr) 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,
|
|
<ErrorDebug error={ctx.err} />,
|
|
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,
|
|
<AppContainer>
|
|
<EnhancedApp Component={EnhancedComponent} router={router} {...props} />
|
|
</AppContainer>,
|
|
ampState
|
|
)
|
|
}
|
|
const documentCtx = { ...ctx, renderPage }
|
|
const docProps: DocumentInitialProps = await loadGetInitialProps(
|
|
Document,
|
|
documentCtx
|
|
)
|
|
// the response might be finished on the getInitialProps call
|
|
if (isResSent(res) && !isSpr) 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<string>()
|
|
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) +
|
|
`<!-- __NEXT_DATA__ -->${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,
|
|
}
|
|
}
|