Skip to content

Commit

Permalink
Add head handling (#41768)
Browse files Browse the repository at this point in the history
- Port tests
- Handle head on initial SSR

<!--
Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change that you're making:
-->

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)

Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: Josh Story <jcs.gnoff@gmail.com>
  • Loading branch information
3 people committed Oct 25, 2022
1 parent bd16ef3 commit f9768a7
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 29 deletions.
8 changes: 7 additions & 1 deletion packages/next/client/components/app-router.tsx
Expand Up @@ -95,6 +95,7 @@ let initialParallelRoutes: CacheNode['parallelRoutes'] =
const prefetched = new Set<string>()

type AppRouterProps = {
initialHead: ReactNode
initialTree: FlightRouterState
initialCanonicalUrl: string
children: ReactNode
Expand All @@ -105,6 +106,7 @@ type AppRouterProps = {
* The global router that wraps the application components.
*/
function Router({
initialHead,
initialTree,
initialCanonicalUrl,
children,
Expand Down Expand Up @@ -361,10 +363,14 @@ function Router({
>
{HotReloader ? (
<HotReloader assetPrefix={assetPrefix}>
{initialHead}
{cache.subTreeData}
</HotReloader>
) : (
cache.subTreeData
<>
{initialHead}
{cache.subTreeData}
</>
)}
</LayoutRouterContext.Provider>
</AppRouterContext.Provider>
Expand Down
66 changes: 38 additions & 28 deletions packages/next/server/app-render.tsx
Expand Up @@ -857,6 +857,40 @@ export async function renderToHTMLOrFlight(
}
}

async function resolveHead(
[segment, parallelRoutes, { head }]: LoaderTree,
parentParams: { [key: string]: any }
): Promise<React.ReactNode> {
// Handle dynamic segment params.
const segmentParam = getDynamicParamFromSegment(segment)
/**
* Create object holding the parent params and current params
*/
const currentParams =
// Handle null case where dynamic param is optional
segmentParam && segmentParam.value !== null
? {
...parentParams,
[segmentParam.param]: segmentParam.value,
}
: // Pass through parent params to children
parentParams
for (const key in parallelRoutes) {
const childTree = parallelRoutes[key]
const returnedHead = await resolveHead(childTree, currentParams)
if (returnedHead) {
return returnedHead
}
}

if (head) {
const Head = await interopDefault(head())
return <Head params={currentParams} />
}

return null
}

const createFlightRouterStateFromLoaderTree = (
[segment, parallelRoutes, { layout }]: LoaderTree,
rootLayoutIncluded = false
Expand Down Expand Up @@ -914,27 +948,19 @@ export async function renderToHTMLOrFlight(
template,
error,
loading,
head,
page,
'not-found': notFound,
},
],
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 All @@ -958,7 +984,6 @@ export async function renderToHTMLOrFlight(
: React.Fragment
const ErrorComponent = error ? await interopDefault(error()) : undefined
const Loading = loading ? await interopDefault(loading()) : undefined
const Head = head ? await interopDefault(head()) : undefined
const isLayout = typeof layout !== 'undefined'
const isPage = typeof page !== 'undefined'
const layoutOrPageMod = isLayout
Expand Down Expand Up @@ -1064,17 +1089,6 @@ export async function renderToHTMLOrFlight(
// Resolve the segment param
const actualSegment = segmentParam ? segmentParam.treeSegment : segment

// collect head pieces
if (typeof Head === 'function') {
collectedHeads.push(() =>
Head({
params: currentParams,
// TODO-APP: allow searchParams?
// ...(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 @@ -1124,7 +1138,6 @@ export async function renderToHTMLOrFlight(
loaderTree: parallelRoutes[parallelRouteKey],
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
collectedHeads,
})

const childProp: ChildProp = {
Expand Down Expand Up @@ -1183,12 +1196,6 @@ 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) {
// TODO: iterate HeadTag children and add a data-path attribute
// so that we can remove elements on client-transition
HeadTags = collectedHeads[collectedHeads.length - 1] as any
}

return (
<>
Expand Down Expand Up @@ -1229,7 +1236,7 @@ export async function renderToHTMLOrFlight(
// Query is only provided to page
{...(isPage ? { searchParams: query } : {})}
/>
{HeadTags ? <HeadTags /> : null}
{/* {HeadTags ? <HeadTags /> : null} */}
</>
)
},
Expand Down Expand Up @@ -1435,6 +1442,8 @@ export async function renderToHTMLOrFlight(
}
: {}

const initialHead = await resolveHead(loaderTree, {})

/**
* A new React Component that renders the provided React Component
* using Flight which can then be rendered to HTML.
Expand All @@ -1448,6 +1457,7 @@ export async function renderToHTMLOrFlight(
assetPrefix={assetPrefix}
initialCanonicalUrl={initialCanonicalUrl}
initialTree={initialTree}
initialHead={initialHead}
>
<ComponentTree />
</AppRouter>
Expand Down

0 comments on commit f9768a7

Please sign in to comment.