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

new head management #108

Merged
merged 4 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions examples/full/layouts/HeadDefault.tsx

This file was deleted.

14 changes: 11 additions & 3 deletions examples/full/pages/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ export { config }

import type { Config } from 'vike/types'
import { LayoutDefault } from '../layouts/LayoutDefault'
import { HeadDefault } from '../layouts/HeadDefault'
import vikeReact from 'vike-react/config'
import logoUrl from '../assets/logo.svg'

// Default configs (can be overridden by pages)
const config = {
// <title>
title: 'My Vike + React App',
Head: HeadDefault,
//*
document: {
title: 'My Vike + React App',
description: 'Demo showcasing Vike + React',
viewport: 'responsive',
icon: logoUrl
},
/*/
favicon: logoUrl,
//*/
// https://vike.dev/Layout
Layout: LayoutDefault,
// https://vike.dev/stream
Expand Down
8 changes: 8 additions & 0 deletions examples/full/pages/star-wars/@id/+data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ export type Data = Awaited<ReturnType<typeof data>>
import fetch from 'node-fetch'
import type { PageContextServer } from 'vike/types'
import type { MovieDetails } from '../types'
import { useDocument } from 'vike-react/useDocument'

const data = async (pageContext: PageContextServer) => {
const document = useDocument()

const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`)
let movie = (await response.json()) as MovieDetails

// Set <title>
document({ title: movie.title })

// We remove data we don't need because the data is passed to the client; we should
// minimize what is sent over the network.
movie = minimize(movie)

return movie
}

Expand Down
9 changes: 0 additions & 9 deletions examples/full/pages/star-wars/@id/+title.ts

This file was deleted.

8 changes: 8 additions & 0 deletions examples/full/pages/star-wars/index/+data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@ export type Data = Awaited<ReturnType<typeof data>>

import fetch from 'node-fetch'
import type { Movie, MovieDetails } from '../types'
import { useDocument } from 'vike-react/useDocument'

const data = async () => {
const document = useDocument()

const response = await fetch('https://brillout.github.io/star-wars/api/films.json')
const moviesData = (await response.json()) as MovieDetails[]

// Set <title>
document({ title: `${moviesData.length} Star Wars Movies` })

// We remove data we don't need because the data is passed to the client; we should
// minimize what is sent over the network.
const movies = minimize(moviesData)

return movies
}

Expand Down
9 changes: 0 additions & 9 deletions examples/full/pages/star-wars/index/+title.ts

This file was deleted.

6 changes: 6 additions & 0 deletions examples/full/pages/streaming/+Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default Page
import React, { Suspense } from 'react'
import { useAsync } from 'react-streaming'
import { Counter } from '../../components/Counter'
import { useDocument } from 'vike-react/useDocument'

function Page() {
return (
Expand Down Expand Up @@ -30,6 +31,11 @@ function MovieList() {
return movies
})

const document = useDocument()
document({
title: `${movies.length} movies`
})

return (
<ol>
{movies.map((movies, index) => (
Expand Down
4 changes: 3 additions & 1 deletion examples/full/pages/without-ssr/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ import type { Config } from 'vike/types'
export default {
// https://vike.dev/ssr
ssr: false,
title: 'No SSR'
document: {
title: 'No SSR'
}
} satisfies Config
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"pnpm": {
"overrides": {
"vike": "0.4.171-commit-75e1588",
"vike-react": "link:./packages/vike-react/",
"vike-react-query": "link:./packages/vike-react-query/"
}
Expand Down
10 changes: 10 additions & 0 deletions packages/vike-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
"exports": {
"./useData": "./dist/hooks/useData.js",
"./usePageContext": "./dist/hooks/usePageContext.js",
"./useDocument": {
"node": "./dist/hooks/useDocument-server.js",
"worker": "./dist/hooks/useDocument-server.js",
"deno": "./dist/hooks/useDocument-server.js",
"browser": "./dist/hooks/useDocument-client.js",
"types": "./dist/hooks/useDocument-server.d.ts"
},
"./ClientOnly": "./dist/components/ClientOnly.js",
".": "./dist/index.js",
"./config": "./dist/+config.js",
Expand Down Expand Up @@ -48,6 +55,9 @@
"usePageContext": [
"./dist/hooks/usePageContext.d.ts"
],
"useDocument": [
"./dist/hooks/useDocument-server.d.ts"
],
"ClientOnly": [
"./dist/components/ClientOnly.d.ts"
],
Expand Down
5 changes: 5 additions & 0 deletions packages/vike-react/src/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default {
onRenderClient: 'import:vike-react/renderer/onRenderClient:onRenderClient',

passToClient: [
'_document',
// https://github.com/vikejs/vike-react/issues/25
process.env.NODE_ENV !== 'production' && '$$typeof'
].filter(isNotFalse),
Expand Down Expand Up @@ -61,6 +62,10 @@ export default {
// Vike already defines the setting 'name', but we redundantly define it here for older Vike versions (otherwise older Vike versions will complain that 'name` is an unknown config).
name: {
env: { config: true }
},
document: {
env: { client: true, server: true },
cumulative: true
}
}
} satisfies Config
13 changes: 13 additions & 0 deletions packages/vike-react/src/hooks/useDocument-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { useDocument }

import type { Document } from '../types/Document.js'
import type { DocumentSetter } from './useDocument-types.js'

function useDocument(): DocumentSetter {
return (document: Document) => {
{
const { title } = document
if (title) window.document.title = title
}
}
}
46 changes: 46 additions & 0 deletions packages/vike-react/src/hooks/useDocument-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export { useDocument }

import { assert } from '../utils/assert.js'
import { usePageContext } from './usePageContext.js'
import { useStream } from 'react-streaming'
import type { Document } from '../types/Document.js'
import type { DocumentSetter } from './useDocument-types.js'
import { getPageContext } from 'vike/getPageContext'

function useDocument(): DocumentSetter {
const documentSetter = (document: Document) => {
;(pageContext as any)._document = document
}

// getPageContext() enables using useDocument() in Vike hooks
let pageContext = getPageContext()
if (pageContext) {
return documentSetter
}

// usePageContext() enables using useDocument() in React components (as React hook)
pageContext = usePageContext()
const stream = useStream()
return (document: Document) => {
const htmlHeadAlreadySet: boolean | undefined = (pageContext as any)._htmlHeadAlreadySet

// No need to use HTML Streaming
if (htmlHeadAlreadySet === false) {
documentSetter(document)
return
}

// <head> already sent to the browser => we send HTML snippets during the HTML Stream
assert(htmlHeadAlreadySet === true)
assert(stream)
{
const { title } = document
if (title) {
assert(typeof title === 'string')
// JSON is safe, thus JSON.stringify() should as well.
const htmlSnippet = `<script>document.title = ${JSON.stringify(title)}</script>`
stream.injectToStream(htmlSnippet)
}
}
}
}
2 changes: 2 additions & 0 deletions packages/vike-react/src/hooks/useDocument-types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import type { Document } from '../types/Document.js'
export type DocumentSetter = (document: Document) => void
27 changes: 27 additions & 0 deletions packages/vike-react/src/renderer/applyDocumentClientSide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export { applyDocumentClientSide }

import type { PageContextClient } from 'vike/types'
import { getDocumentElementsIsomoprh } from './getDocument.js'

function applyDocumentClientSide(pageContext: PageContextClient): void {
const { title, lang, favicon } = getDocumentElementsIsomoprh(pageContext)
// We skip if the value is undefined because we shouldn't remove values set in HTML (by the Head setting).
if (title !== undefined) window.document.title = title
if (lang !== undefined) window.document.documentElement.lang = lang
if (favicon !== undefined) setFavicon(favicon)
}

// https://stackoverflow.com/questions/260857/changing-website-favicon-dynamically/260876#260876
function setFavicon(faviconUrl: string | null) {
let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']")
if (!faviconUrl) {
if (link) window.document.head.removeChild(link)
return
}
if (!link) {
link = window.document.createElement('link')
link.rel = 'icon'
window.document.head.appendChild(link)
}
link.href = faviconUrl
}
73 changes: 73 additions & 0 deletions packages/vike-react/src/renderer/getDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export { getDocument }
export { getDocumentElementsIsomoprh }

import type { PageContext } from 'vike/types'
import type { Document } from '../types/Document.js'
import { assert } from '../utils/assert.js'

function getDocumentElementsIsomoprh(pageContext: PageContext): {
title: undefined | string
lang: undefined | string
favicon: undefined | string
} {
const document = getDocument(pageContext)

let title = document.title
let lang = document.locale
// TODO
const favicon = document.icon as string | undefined

return {
title,
lang,
favicon
}
}

function getDocument(pageContext: PageContext): Document {
const documentValues = pageContext.from.configsCumulative.document?.values ?? []
let newInterfaceUsedAt: `at ${string}` | 'with useDocument()' | undefined
const documentMerged: Document = {}
documentValues.reverse().forEach((v) => {
newInterfaceUsedAt = `at ${v.definedAt}`
// We don't valitae but type cast instead in order to save client-side KBs.
const document = v.value as Document
forEach(document, (key, value) => {
assert(value !== undefined)
documentMerged[key] = value
})
})
{
// We don't valitae but type cast instead in order to save client-side KBs.
const documentFromPageContext: Document = '_document' in pageContext ? (pageContext as any)._document : {}
forEach(documentFromPageContext, (key, value) => {
if (!newInterfaceUsedAt) {
newInterfaceUsedAt = 'with useDocument()'
}
documentMerged[key] = value
})
}

// Assert user doesn't mix old and new interface.
if (newInterfaceUsedAt) {
;(['title', 'lang', 'favicon'] as const).forEach((prop) => {
if (pageContext.from.configsStandard[prop]) {
const definedAtOld = pageContext.from.configsStandard[prop]!.definedAt
assert(definedAtOld)
throw new Error(
`You're using the new interface ${newInterfaceUsedAt} as well as the old interface at ${definedAtOld} but you cannot use both at the same time: either always use the new interface, or always use the old interface.`
)
}
assert(!(prop in pageContext.config))
})
}

return documentMerged
}

function forEach<Obj extends object>(
obj: Obj,
iterator: <Key extends keyof Obj>(key: Key, val: Obj[Key]) => void
): void {
Object.entries(obj).forEach(([key, val]) => iterator(key as keyof Obj, val))
}
Loading
Loading