Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
brillout committed Apr 25, 2024
1 parent b749e6c commit e8d9f77
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 26 deletions.
4 changes: 3 additions & 1 deletion examples/full/pages/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import vikeReact from 'vike-react/config'
// Default configs (can be overridden by pages)
const config = {
// <title>
title: 'My Vike + React App',
document: {
title: 'My Vike + React App'
},
Head: HeadDefault,
// https://vike.dev/Layout
Layout: LayoutDefault,
Expand Down
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
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-ea3808a",
"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-browser.js",
"types": "./dist/hooks/useDocument-server.js"
},
"./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.d.ts"
],
"ClientOnly": [
"./dist/components/ClientOnly.d.ts"
],
Expand Down
4 changes: 4 additions & 0 deletions packages/vike-react/src/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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) document.title = title
}
}
}
31 changes: 31 additions & 0 deletions packages/vike-react/src/hooks/useDocument-server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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'

function useDocument(): DocumentSetter {
const pageContext = usePageContext()
const stream = useStream()
return (document: Document) => {
const htmlHeadAlreadySet: boolean | undefined = (pageContext as any)._htmlHeadAlreadySet
if (htmlHeadAlreadySet === false) {
;(pageContext as any)._document = document
} else {
assert(htmlHeadAlreadySet === true)
assert(stream)
{
const { title } = document
if (title) {
assert(typeof title === 'string')
// JSON is safe
const titleSafe = JSON.stringify(title)
const htmlSnippet = `<script>document.title = ${titleSafe}</script>`
stream.injectToStream(htmlSnippet)
}
}
}
}
}
1 change: 1 addition & 0 deletions packages/vike-react/src/hooks/useDocument-types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type DocumentSetter = (document: Document) => void
19 changes: 19 additions & 0 deletions packages/vike-react/src/renderer/getDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export { getDocument }

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

function getDocument(pageContext: PageContext): Document {
/* TODO: Vike namespace ConfigResolved
pageContext.config.document
*/
const values = pageContext.from.configsCumulative.document?.values ?? []
const document: Document = {}
values.forEach((v) => {
const val =
// We don't do proper assertUsage() validation, but we just type case instead in order to save client-side KBs.
v.value as Document
Object.assign(document, val)
})
return document
}
10 changes: 7 additions & 3 deletions packages/vike-react/src/renderer/onRenderClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ReactDOM from 'react-dom/client'
import { getHeadSetting } from './getHeadSetting.js'
import type { OnRenderClientSync } from 'vike/types'
import { getPageElement } from './getPageElement.js'
import { getDocument } from './getDocument.js'

let root: ReactDOM.Root
const onRenderClient: OnRenderClientSync = (pageContext): ReturnType<OnRenderClientSync> => {
Expand Down Expand Up @@ -32,9 +33,12 @@ const onRenderClient: OnRenderClientSync = (pageContext): ReturnType<OnRenderCli
} else {
// Client-side navigation

const title = getHeadSetting('title', pageContext) || ''
const lang = getHeadSetting('lang', pageContext) || 'en'
const favicon = getHeadSetting('favicon', pageContext)
let title = getHeadSetting('title', pageContext)
let lang = getHeadSetting('lang', pageContext) || 'en'
let favicon = getHeadSetting('favicon', pageContext)

const doc = getDocument(pageContext)
title = title ?? doc.title ?? ''

// We skip if the value is undefined because we shouldn't remove values set in HTML (by the Head setting).
// - This also means that previous values will leak: upon client-side navigation, the title set by the previous page won't be removed if the next page doesn't override it. But that's okay because usually pages always have a favicon and title, which means that previous values are always overriden. Also, as a workaround, the user can set the value to `null` to ensure that previous values are overriden.
Expand Down
19 changes: 13 additions & 6 deletions packages/vike-react/src/renderer/onRenderHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,13 @@ import { getPageElement } from './getPageElement.js'
import { PageContextProvider } from '../hooks/usePageContext.js'
import React from 'react'
import type { OnRenderHtmlAsync } from 'vike/types'
import { getDocument } from './getDocument.js'

checkVikeVersion()
addEcosystemStamp()

const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType<OnRenderHtmlAsync> => {
const title = getHeadSetting('title', pageContext)
const favicon = getHeadSetting('favicon', pageContext)
const lang = getHeadSetting('lang', pageContext) || 'en'

const titleTag = !title ? '' : escapeInject`<title>${title}</title>`
const faviconTag = !favicon ? '' : escapeInject`<link rel="icon" href="${favicon}" />`
;(pageContext as any)._htmlHeadAlreadySet = false

const Head = pageContext.config.Head || (() => <></>)
const head = (
Expand Down Expand Up @@ -50,6 +46,17 @@ const onRenderHtml: OnRenderHtmlAsync = async (pageContext): ReturnType<OnRender
}
}

let title = getHeadSetting('title', pageContext)
let favicon = getHeadSetting('favicon', pageContext)
let lang = getHeadSetting('lang', pageContext) || 'en'

const document = getDocument(pageContext)
title = title ?? (pageContext as any)._document?.title ?? document.title
;(pageContext as any)._htmlHeadAlreadySet = true

const titleTag = !title ? '' : escapeInject`<title>${title}</title>`
const faviconTag = !favicon ? '' : escapeInject`<link rel="icon" href="${favicon}" />`

const documentHtml = escapeInject`<!DOCTYPE html>
<html lang='${lang}'>
<head>
Expand Down
3 changes: 3 additions & 0 deletions packages/vike-react/src/types/Config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// https://vike.dev/meta#typescript
import type { PageContextClient } from 'vike/types'
import type { Document } from '../types/Document.js'

declare global {
// As a Vike user, use Vike.Config instead of VikePackages.ConfigVikeReact (see https://vike.dev/meta#typescript)
Expand Down Expand Up @@ -27,6 +28,8 @@ declare global {
*/
Wrapper?: (props: { children: React.ReactNode }) => React.ReactNode

document?: Document

/** &lt;title>${title}&lt;/title> */
title?: string

Expand Down
102 changes: 102 additions & 0 deletions packages/vike-react/src/types/Document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
export type { Document }

// TODO:
// - viewport default
// - locale default
// - <meta property="og:type" content="website">
// - icon large, small, ...
// - preview images with different sizes?
// - dir? https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
// - Can lang be set to en-US instead of en?
type Document = {
title?: string
description?: string
viewport?: string | null
/**
*
* <html lang="en">
* <meta property="og:locale" content="en_GB">
*
*/
locale?: string

htmlTagAttributes?: Record<string, string>
bodyTagAttributes?: Record<string, string>
rootTagAttributes?: Record<string, string>

/** Website icon (aka favicon).
*
* <link rel="icon" href="/favicon.ico">
* <link rel="apple-touch-icon" href="/apple-touch-icon.png">
*/
icon?:
| string
| {
url: string
sizes?: string
apple?: boolean
}[]

/**
* Preview image for social sharing.
*
* <meta property="og:image" content="https://vitejs.dev/og-image.png">
* <meta name="twitter:card" content="summary_large_image">
*/
previewImage?:
| string
| {
url: string
// <meta name="twitter:card" content="summary_large_image">
size?: 'summary_large_image' | '??'
alt?: string
}

// <meta name="twitter:site" content="@vite_js">
twitter?: string

/**
* Canonical URL.
*
* <meta property="og:url" content="https://www.mywebsite.com/page">
* <link rel="canonical" href="https://www.mywebsite.com/page">
*/
canonical?: string

/**
* The web manifest URL.
*
* `<link rel="manifest" href="${document.manifest}">`
*
* https://developer.mozilla.org/en-US/docs/Web/Manifest
* https://vike.dev/document
*/
manifest?: string

/**
* The UI frame color for mobile.
*
* `<meta name="theme-color" content="${document.themeColor}">`
*
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
* https://vike.dev/document
*/
themeColor?:
| string
| {
/**
* `<meta name="theme-color" content="${document.themeColor.dark}" media="(prefers-color-scheme: dark)">`
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
* https://vike.dev/document
*/
dark: string
/**
* `<meta name="theme-color" content="${document.themeColor.light}" media="(prefers-color-scheme: light)">`
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
* https://vike.dev/document
*/
light: string
}
}
Loading

0 comments on commit e8d9f77

Please sign in to comment.