Emit late streaming meta tags (#47207)

Currently if `notFound()` or `redirect()` is called when the shell was already sent out, we can no longer change the status code and head tags. In that case we inject these specific meta tags into the HTML stream so specific agents can read them.

fix NEXT-220 ([link](https://linear.app/vercel/issue/NEXT-220))
This commit is contained in:
Shu Ding 2023-03-17 15:37:00 +01:00 committed by GitHub
parent 694e7f9e80
commit 51b1fe3d2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 85 additions and 4 deletions

View file

@ -1737,7 +1737,35 @@ export async function renderToHTMLOrFlight(
)
let polyfillsFlushed = false
const getServerInsertedHTML = (): Promise<string> => {
let flushedErrorMetaTagsUntilIndex = 0
const getServerInsertedHTML = () => {
// Loop through all the errors that have been captured but not yet
// flushed.
const errorMetaTags = []
for (
;
flushedErrorMetaTagsUntilIndex < allCapturedErrors.length;
flushedErrorMetaTagsUntilIndex++
) {
const error = allCapturedErrors[flushedErrorMetaTagsUntilIndex]
if (isNotFoundError(error)) {
errorMetaTags.push(
<meta name="robots" content="noindex" key={error.digest} />
)
} else if (isRedirectError(error)) {
const redirectUrl = getURLFromRedirectError(error)
if (redirectUrl) {
errorMetaTags.push(
<meta
httpEquiv="refresh"
content={`0;url=${redirectUrl}`}
key={error.digest}
/>
)
}
}
}
const flushed = renderToString(
<>
{Array.from(serverInsertedHTMLCallbacks).map((callback) =>
@ -1756,6 +1784,7 @@ export async function renderToHTMLOrFlight(
/>
)
})}
{errorMetaTags}
</>
)
polyfillsFlushed = true

View file

@ -0,0 +1,41 @@
import { Suspense } from 'react'
import { redirect } from 'next/navigation'
function createSuspenseyComponent(Component, { timeout = 0, expire = 10 }) {
let result
let promise
return function Data() {
if (result) return result
if (!promise)
promise = new Promise((resolve) => {
setTimeout(() => {
result = <Component />
setTimeout(() => {
result = undefined
promise = undefined
}, expire)
resolve()
}, timeout)
})
throw promise
}
}
function Redirect() {
redirect('/redirect/result')
return <></>
}
const SuspenseyRedirect = createSuspenseyComponent(Redirect, {
timeout: 300,
})
export default function () {
return (
<div className="suspense">
<Suspense fallback="fallback">
<SuspenseyRedirect />
</Suspense>
</div>
)
}

View file

@ -105,9 +105,6 @@ createNextDescribe(
).toBe('noindex')
})
it('should trigger not-found while streaming', async () => {
const initialHtml = await next.render('/not-found/suspense')
expect(initialHtml).not.toContain('noindex')
const browser = await next.browser('/not-found/suspense')
expect(
await browser.waitForElementByCss('#not-found-component').text()
@ -261,5 +258,19 @@ createNextDescribe(
}
})
})
describe('SEO', () => {
it('should emit noindex meta tag for not found page when streaming', async () => {
const html = await next.render('/not-found/suspense')
expect(html).toContain('<meta name="robots" content="noindex"/>')
})
it('should emit refresh meta tag for redirect page when streaming', async () => {
const html = await next.render('/redirect/suspense')
expect(html).toContain(
'<meta http-equiv="refresh" content="0;url=/redirect/result"/>'
)
})
})
}
)