Skip to content
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
40 changes: 40 additions & 0 deletions examples/full/.testRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import assert from 'node:assert'
let isProd: boolean

const titleDefault = 'My Vike + React App'
// Set by <Config description> inside the <Image> component of /images
const descriptionImages = 'Logo created by Romuald Brillout'
const pages = {
'/': {
title: titleDefault,
Expand All @@ -30,6 +32,7 @@ const pages = {
},
'/without-ssr': {
title: 'No SSR',
description: 'Page rendered only in the browser',
text: 'This page is rendered only in the browser',
counter: true,
noSSR: true,
Expand All @@ -48,6 +51,7 @@ function testRun(cmd: `pnpm run ${'dev' | 'preview'}`) {
testPages()
testPageNavigation_betweenWithSSRAndWithout()
testPageNavigation_titleUpdate()
testPageNavigation_descriptionUpdate()
testUseConfig()
testReactSetting()
testClientOnly()
Expand Down Expand Up @@ -123,9 +127,43 @@ async function expectTitle(title: string) {
await autoRetry(async () => {
const titleActual = await page.evaluate(() => window.document.title)
expect(titleActual).toBe(title)
expect(await getMetaContent('meta[property="og:title"]')).toBe(title)
})
}

// The description tags are updated upon client-side navigation. https://github.com/vikejs/vike/issues/3524
function testPageNavigation_descriptionUpdate() {
test('description update client-side page navigation', async () => {
await page.goto(getServerUrl() + '/')
await testCounter()
await expectDescription(null)
// Set by +description
await page.click('a:has-text("Without SSR")')
await testCounter()
await expectDescription(pages['/without-ssr'].description)
// Set by useConfig() inside +data()
await page.click('a[href="/star-wars"]')
await expectDescription(pages['/star-wars'].description)
// Set by <Config> inside UI components
await page.click('a:has-text("useConfig()")')
await testCounter()
await expectDescription(descriptionImages)
await page.click('a:has-text("Without SSR")')
await testCounter()
await expectDescription(pages['/without-ssr'].description)
await ensureWasClientSideRouted('/pages/index')
})
}
async function expectDescription(description: string | null) {
await autoRetry(async () => {
expect(await getMetaContent('meta[name="description"]')).toBe(description)
expect(await getMetaContent('meta[property="og:description"]')).toBe(description)
})
}
async function getMetaContent(selector: string) {
return await page.evaluate((selector) => document.querySelector(selector)?.getAttribute('content') ?? null, selector)
}

function testPages() {
Object.entries(pages).forEach(([url, pageInfo]) => {
testPage({ url, ...pageInfo })
Expand Down Expand Up @@ -202,6 +240,8 @@ function testUseConfig() {
test('useConfig() HTML', async () => {
const html = await fetchHtml('/images')
expect(getTitle(html)).toBe(titleImages)
expect(html).toContain(`<meta name="description" content="${descriptionImages}" />`)
expect(html).toContain(`<meta property="og:description" content="${descriptionImages}" />`)
expect(html).toMatch(
partRegex`<script type="application/ld+json">{"@context":"https://schema.org/","contentUrl":{"src":"${getAssetUrl(
'logo-new.svg',
Expand Down
2 changes: 1 addition & 1 deletion examples/full/pages/images/+Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function Image({ src, author }: { src: string; author: string }) {
return (
<>
<img src={src} height={48} style={{ verticalAlign: 'middle', marginLeft: 10 }} />
<Config title={`Image created by ${author}`} />
<Config title={`Image created by ${author}`} description={`Logo created by ${author}`} />
<Head>
<script
type="application/ld+json"
Expand Down
1 change: 1 addition & 0 deletions examples/full/pages/without-ssr/+config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export default {
// https://vike.dev/ssr
ssr: false,
title: 'No SSR',
description: 'Page rendered only in the browser',
} satisfies Config
2 changes: 1 addition & 1 deletion packages/vike-react/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const config = {
env: { server: true, client: true },
},
description: {
env: { server: true },
env: { server: true, client: true },
},
image: {
env: { server: true },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Settings the client-side applies upon navigation, see applyHeadSettings(). The others are HTML-only.
export const configsClientSide = ['title', 'lang'] as const
export const configsClientSide = ['title', 'lang', 'description'] as const
4 changes: 2 additions & 2 deletions packages/vike-react/src/hooks/useConfig/useConfig-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ function setPageContextConfigViaHook(config: ConfigViaHook, pageContext: PageCon
}

function applyHead(config: ConfigViaHook) {
const { title, lang } = config
applyHeadSettings(title, lang)
const { title, lang, description } = config
applyHeadSettings(title, lang, description)
}
26 changes: 24 additions & 2 deletions packages/vike-react/src/integration/applyHeadSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,29 @@ type Value = string | null | undefined
// - Most of the time, the user sets a default himself (i.e. a value defined at /pages/+config.js)
// - If he doesn't have a default then he can use `null` to opt into Vike's defaults

function applyHeadSettings(title: Value, lang: Value) {
if (title !== undefined) document.title = title || ''
function applyHeadSettings(title: Value, lang: Value, description: Value) {
if (title !== undefined) {
document.title = title || ''
setMetaTag('property', 'og:title', title)
}
if (lang !== undefined) document.documentElement.lang = lang || 'en'
if (description !== undefined) {
setMetaTag('name', 'description', description)
setMetaTag('property', 'og:description', description)
}
}

// An empty value removes the tag (the server-side doesn't render the tag either)
function setMetaTag(attrName: 'name' | 'property', attrValue: string, content: string | null) {
let tag = document.head.querySelector<HTMLMetaElement>(`meta[${attrName}="${attrValue}"]`)
if (!content) {
tag?.remove()
return
}
if (!tag) {
tag = document.createElement('meta')
tag.setAttribute(attrName, attrValue)
document.head.appendChild(tag)
}
tag.content = content
}
3 changes: 2 additions & 1 deletion packages/vike-react/src/integration/onRenderClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ async function onRenderClient(pageContext: PageContextClient & PageContextIntern
function applyHead(pageContext: PageContextClient) {
const title = getHeadSetting<string | null>('title', pageContext)
const lang = getHeadSetting<string | null>('lang', pageContext)
applyHeadSettings(title, lang)
const description = getHeadSetting<string | null>('description', pageContext)
applyHeadSettings(title, lang, description)
}

// Global callback, attached once upon hydration.
Expand Down
2 changes: 1 addition & 1 deletion packages/vike-react/src/types/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ declare global {
*
* https://vike.dev/description
*/
description?: string | null | ((pageContext: PageContextServer) => string | null | undefined)
description?: string | null | ((pageContext: PageContext_) => string | null | undefined)

/**
* Set the page's preview image upon URL sharing.
Expand Down
Loading