Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial head handling in app #41607

Merged
merged 10 commits into from Oct 25, 2022
23 changes: 23 additions & 0 deletions packages/next/server/app-render.tsx
Expand Up @@ -914,12 +914,19 @@ export async function renderToHTMLOrFlight(
parentParams,
firstItem,
rootLayoutIncluded,
collectedHeads = [],
}: {
createSegmentPath: CreateSegmentPath
loaderTree: LoaderTree
parentParams: { [key: string]: any }
rootLayoutIncluded?: boolean
firstItem?: boolean
collectedHeads?: Array<
(ctx: {
params?: Record<string, string | string[]>
searchParams?: Record<string, string | string[]>
}) => Promise<React.ElementType>
>
}): Promise<{ Component: React.ComponentType }> => {
// TODO-APP: enable stylesheet per layout/page
const stylesheets: string[] = layoutOrPagePath
Expand Down Expand Up @@ -1010,6 +1017,16 @@ export async function renderToHTMLOrFlight(
// Resolve the segment param
const actualSegment = segmentParam ? segmentParam.treeSegment : segment

// collect head pieces
if (typeof layoutOrPageMod?.Head === 'function') {
collectedHeads.push(() =>
layoutOrPageMod.Head({
params: currentParams,
...(isPage ? { searchParams: query } : {}),
})
)
}

// This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down
const parallelRouteMap = await Promise.all(
Object.keys(parallelRoutes).map(
Expand Down Expand Up @@ -1059,6 +1076,7 @@ export async function renderToHTMLOrFlight(
loaderTree: parallelRoutes[parallelRouteKey],
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
collectedHeads,
})

const childProp: ChildProp = {
Expand Down Expand Up @@ -1117,9 +1135,14 @@ export async function renderToHTMLOrFlight(
// Add extra cache busting (DEV only) for https://github.com/vercel/next.js/issues/5860
// See also https://bugs.webkit.org/show_bug.cgi?id=187726
const cacheBustingUrlSuffix = dev ? `?ts=${Date.now()}` : ''
let HeadTags
if (rootLayoutAtThisLevel) {
HeadTags = collectedHeads[collectedHeads.length - 1] as any
}

return (
<>
{HeadTags ? <HeadTags /> : null}
ijjk marked this conversation as resolved.
Show resolved Hide resolved
{preloadedFontFiles.map((fontFile) => {
const ext = /\.(woff|woff2|eot|ttf|otf)$/.exec(fontFile)![1]
return (
Expand Down
95 changes: 95 additions & 0 deletions test/e2e/app-dir/head.test.ts
@@ -0,0 +1,95 @@
import path from 'path'
import cheerio from 'cheerio'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP } from 'next-test-utils'

describe('app dir head', () => {
if ((global as any).isNextDeploy) {
it('should skip next deploy for now', () => {})
return
}

if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
it('should skip for react v17', () => {})
return
}
let next: NextInstance

function runTests() {
beforeAll(async () => {
next = await createNext({
files: new FileRef(path.join(__dirname, 'head')),
dependencies: {
react: 'experimental',
'react-dom': 'experimental',
},
skipStart: true,
})

await next.start()
})
afterAll(() => next.destroy())

it('should use head from index page', async () => {
const html = await renderViaHTTP(next.url, '/')
const $ = cheerio.load(html)
const headTags = $('head').children().toArray()

expect(headTags.find((el) => el.attribs.src === '/hello.js')).toBeTruthy()
expect(
headTags.find((el) => el.attribs.src === '/another.js')
).toBeTruthy()
})

it('should not use head from layout when on page', async () => {
const html = await renderViaHTTP(next.url, '/blog')
const $ = cheerio.load(html)
const headTags = $('head').children().toArray()

expect(
headTags.find((el) => el.attribs.src === '/hello3.js')
).toBeTruthy()
expect(headTags.find((el) => el.attribs.src === '/hello1.js')).toBeFalsy()
expect(headTags.find((el) => el.attribs.src === '/hello2.js')).toBeFalsy()
expect(
headTags.find((el) => el.attribs.src === '/another.js')
).toBeTruthy()
})

it('should use head from layout when not on page', async () => {
const html = await renderViaHTTP(next.url, '/blog/about')
const $ = cheerio.load(html)
const headTags = $('head').children().toArray()

expect(
headTags.find((el) => el.attribs.src === '/hello1.js')
).toBeTruthy()
expect(
headTags.find((el) => el.attribs.src === '/hello2.js')
).toBeTruthy()
expect(
headTags.find((el) => el.attribs.src === '/another.js')
).toBeTruthy()
})

it('should pass params to head for dynamic path', async () => {
const html = await renderViaHTTP(next.url, '/blog/post-1')
const $ = cheerio.load(html)
const headTags = $('head').children().toArray()

expect(
headTags.find(
(el) =>
el.attribs.src === '/hello3.js' &&
el.attribs['data-slug'] === 'post-1'
)
).toBeTruthy()
expect(
headTags.find((el) => el.attribs.src === '/another.js')
).toBeTruthy()
})
}

runTests()
})
18 changes: 18 additions & 0 deletions test/e2e/app-dir/head/app/blog/[slug]/page.js
@@ -0,0 +1,18 @@
export default function Page() {
return (
<>
<p id="page">dynamic blog page</p>
</>
)
}

export async function Head({ params }) {
return (
<>
<script async src="/hello3.js" data-slug={params.slug} />
{/* TODO-APP: enable after react is updated to handle
other head tags
<title>hello from dynamic blog page {params.slug}</title> */}
</>
)
}
7 changes: 7 additions & 0 deletions test/e2e/app-dir/head/app/blog/about/page.js
@@ -0,0 +1,7 @@
export default function Page() {
return (
<>
<p id="page">blog about page</p>
</>
)
}
21 changes: 21 additions & 0 deletions test/e2e/app-dir/head/app/blog/layout.js
@@ -0,0 +1,21 @@
export default function Layout({ children }) {
return (
<>
<p id="layout">blog layout</p>
{children}
</>
)
}

export async function Head() {
return (
<>
<script async src="/hello1.js" />
<script async src="/hello2.js" />
{/* TODO-APP: enable after react is updated to handle
other head tags
<title>hello from blog layout</title>
<meta name="description" content="a neat blog" /> */}
</>
)
}
18 changes: 18 additions & 0 deletions test/e2e/app-dir/head/app/blog/page.js
@@ -0,0 +1,18 @@
export default function Page() {
return (
<>
<p id="page">blog page</p>
</>
)
}

export async function Head() {
return (
<>
<script async src="/hello3.js" />
{/* TODO-APP: enable after react is updated to handle
other head tags
<title>hello from blog page</title> */}
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/head/app/layout.js
@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html lang="en">
<head>
<script async src="/another.js" />
</head>
<body>{children}</body>
</html>
)
}
19 changes: 19 additions & 0 deletions test/e2e/app-dir/head/app/page.js
@@ -0,0 +1,19 @@
export default function Page() {
return (
<>
<p id="page">index page</p>
</>
)
}

export async function Head() {
return (
<>
<script async src="/hello.js" />
{/* TODO-APP: enable after react is updated to handle
other head tags
<title>hello from index</title>
<meta name="description" content="an index page" /> */}
</>
)
}
17 changes: 17 additions & 0 deletions test/e2e/app-dir/head/next.config.js
@@ -0,0 +1,17 @@
module.exports = {
experimental: {
appDir: true,
},
// assetPrefix: '/assets',
rewrites: async () => {
return {
// beforeFiles: [ { source: '/assets/:path*', destination: '/:path*' } ],
afterFiles: [
{
source: '/rewritten-to-dashboard',
destination: '/dashboard',
},
],
}
},
}
4 changes: 4 additions & 0 deletions test/e2e/app-dir/head/public/another.js
@@ -0,0 +1,4 @@
if (typeof window !== 'undefined') {
window.fromAnother = true
}
console.log('another')
4 changes: 4 additions & 0 deletions test/e2e/app-dir/head/public/hello.js
@@ -0,0 +1,4 @@
if (typeof window !== 'undefined') {
window.fromHello = true
}
console.log('hello')
4 changes: 4 additions & 0 deletions test/e2e/app-dir/head/public/hello1.js
@@ -0,0 +1,4 @@
if (typeof window !== 'undefined') {
window.fromHello1 = true
}
console.log('hello1')
4 changes: 4 additions & 0 deletions test/e2e/app-dir/head/public/hello2.js
@@ -0,0 +1,4 @@
if (typeof window !== 'undefined') {
window.fromHello2 = true
}
console.log('hello2')
4 changes: 4 additions & 0 deletions test/e2e/app-dir/head/public/hello3.js
@@ -0,0 +1,4 @@
if (typeof window !== 'undefined') {
window.fromHello3 = true
}
console.log('hello3')