per-page-title

This commit is contained in:
Ulf Gebhardt 2024-01-28 12:25:43 +01:00
parent 335de9753b
commit 5fd3b4f7ad
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
20 changed files with 174 additions and 91 deletions

View File

@ -1,3 +1,5 @@
# META
PUBLIC_ENV__META__DEFAULT_TITLE="IT4C"
PUBLIC_ENV__META__DEFAULT_DESCRIPTION="IT4C Frontend Boilerplate"
PUBLIC_ENV__META__BASE_URL="http://localhost:3000"
PUBLIC_ENV__META__DEFAULT_AUTHOR="IT Team 4 Change"
PUBLIC_ENV__META__DEFAULT_DESCRIPTION="IT4C Frontend Boilerplate"
PUBLIC_ENV__META__DEFAULT_TITLE="IT4C"

View File

@ -3,4 +3,14 @@ export default {
clientRouting: true,
prefetchStaticAssets: 'viewport',
passToClient: ['pageProps', /* 'urlPathname', */ 'routeParams'],
meta: {
title: {
// Make the value of `title` available on both the server- and client-side
env: { server: true, client: true },
},
description: {
// Make the value of `description` available only on the server-side
env: { server: true },
},
},
}

View File

@ -1,15 +1,18 @@
import { createApp } from './app'
import { PageContext } from 'vike/types'
import type { PageContext, VikePageContext } from '#types/PageContext'
import { createApp } from './app'
import { getTitle } from './utils'
let instance: ReturnType<typeof createApp>
/* async */ function render(pageContext: VikePageContext & PageContext) {
/* async */ function render(pageContext: PageContext) {
if (!instance) {
instance = createApp(pageContext)
instance.app.mount('#app')
} else {
instance.app.changePage(pageContext)
}
document.title = getTitle(pageContext)
}
export default render

View File

@ -1,12 +1,15 @@
import { renderToString as renderToString_ } from '@vue/server-renderer'
import { escapeInject, dangerouslySkipEscape } from 'vike/server'
import { PageContext, PageContextServer } from 'vike/types'
import logoUrl from '#assets/favicon.ico'
import image from '#assets/it4c-logo2-clean-bg_alpha-128x128.png'
import { META } from '#src/env'
import { createApp } from './app'
import type { PageContextServer, PageContext } from '#types/PageContext'
import { createApp } from './app'
import { getDescription, getTitle } from './utils'
import type { App } from 'vue'
async function render(pageContext: PageContextServer & PageContext) {
@ -17,9 +20,8 @@ async function render(pageContext: PageContextServer & PageContext) {
const appHtml = await renderToString(app)
// See https://vike.dev/head
const { documentProps } = pageContext.exports
const title = (documentProps && documentProps.title) || META.DEFAULT_TITLE
const desc = (documentProps && documentProps.description) || META.DEFAULT_DESCRIPTION
const title = getTitle(pageContext)
const description = getDescription(pageContext)
const documentHtml = escapeInject`<!DOCTYPE html>
<html lang="${locale}">
@ -27,7 +29,23 @@ async function render(pageContext: PageContextServer & PageContext) {
<meta charset="UTF-8" />
<link rel="icon" href="${logoUrl}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${desc}" />
<meta name="description" content="${description}" />
<meta name="author" content="${META.DEFAULT_AUTHOR}">
<meta property="og:title" content="${title}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="${META.BASE_URL}" />
<meta property="og:description" content="${description}" />
<meta property="og:image" content="${META.BASE_URL}${image}" />
<meta property="og:image:alt" content="${title}" />
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="601"/>
<meta name="twitter:card" content="summary_large_image" />
<!--<meta name="twitter:site" content="@YourTwitterUsername" />-->
<meta name="twitter:title" content="${title}" />
<meta name="twitter:text:title" content="${title}" />
<meta name="twitter:description" content="${description}" />
<meta name="twitter:image" content="${META.BASE_URL}${image}" />
<meta name="twitter:image:alt" content="${title}" />
<title>${title}</title>
</head>
<body>

View File

@ -7,11 +7,11 @@ import i18n from '#plugins/i18n'
import pinia from '#plugins/pinia'
import CreateVuetify from '#plugins/vuetify'
import type { PageContext, VikePageContext } from '#types/PageContext'
import { PageContext } from 'vike/types'
const vuetify = CreateVuetify(i18n)
function createApp(pageContext: VikePageContext & PageContext, isClient = true) {
function createApp(pageContext: PageContext, isClient = true) {
// eslint-disable-next-line no-use-before-define
let rootComponent: InstanceType<typeof PageWithWrapper>
const PageWithWrapper = defineComponent({
@ -47,7 +47,7 @@ function createApp(pageContext: VikePageContext & PageContext, isClient = true)
app.use(vuetify)
objectAssign(app, {
changePage: (pageContext: VikePageContext & PageContext) => {
changePage: (pageContext: PageContext) => {
Object.assign(pageContextReactive, pageContext)
rootComponent.Page = markRaw(pageContext.Page)
rootComponent.pageProps = markRaw(pageContext.pageProps || {})

View File

@ -1,13 +1,12 @@
// `usePageContext` allows us to access `pageContext` in any Vue component.
// See https://vike.dev/pageContext-anywhere
import { PageContext } from 'vike/types'
import { inject } from 'vue'
import { PageContext, VikePageContext } from '#types/PageContext'
import type { App, InjectionKey } from 'vue'
export const vikePageContext: InjectionKey<VikePageContext & PageContext> = Symbol('pageContext')
export const vikePageContext: InjectionKey<PageContext> = Symbol('pageContext')
function usePageContext() {
const pageContext = inject(vikePageContext)
@ -15,7 +14,7 @@ function usePageContext() {
return pageContext
}
function setPageContext(app: App, pageContext: VikePageContext & PageContext) {
function setPageContext(app: App, pageContext: PageContext) {
app.provide(vikePageContext, pageContext)
}

19
renderer/utils.ts Normal file
View File

@ -0,0 +1,19 @@
import { PageContext } from 'vike/types'
import { META } from '#src/env'
function getTitle(pageContext: PageContext) {
// The value exported by /pages/**/+title.js is available at pageContext.config.title
const val = pageContext.config.title
if (typeof val === 'string') return val
if (typeof val === 'function') return String(val(pageContext))
return META.DEFAULT_TITLE
}
function getDescription(pageContext: PageContext) {
const val = pageContext.config.description
if (typeof val === 'string') return val
if (typeof val === 'function') return val(pageContext)
return META.DEFAULT_DESCRIPTION
}
export { getTitle, getDescription }

View File

@ -5,8 +5,10 @@ import { META } from './env'
describe('env', () => {
it('has correct default values', () => {
expect(META).toEqual({
DEFAULT_TITLE: 'IT4C',
BASE_URL: 'http://localhost:3000',
DEFAULT_AUTHOR: 'IT Team 4 Change',
DEFAULT_DESCRIPTION: 'IT4C Frontend Boilerplate',
DEFAULT_TITLE: 'IT4C',
})
})
})

View File

@ -1,8 +1,10 @@
const META = {
DEFAULT_TITLE: (import.meta.env.PUBLIC_ENV__META__DEFAULT_TITLE as string) ?? 'IT4C',
DEFAULT_DESCRIPTION:
(import.meta.env.PUBLIC_ENV__META__DEFAULT_DESCRIPTION as string) ??
'IT4C Frontend Boilerplate',
BASE_URL: (import.meta.env.PUBLIC_ENV__META__BASE_URL ?? 'http://localhost:3000') as string,
DEFAULT_AUTHOR: (import.meta.env.PUBLIC_ENV__META__DEFAULT_AUTHOR ??
'IT Team 4 Change') as string,
DEFAULT_DESCRIPTION: (import.meta.env.PUBLIC_ENV__META__DEFAULT_DESCRIPTION ??
'IT4C Frontend Boilerplate') as string,
DEFAULT_TITLE: (import.meta.env.PUBLIC_ENV__META__DEFAULT_TITLE ?? 'IT4C') as string,
}
export { META }

View File

@ -0,0 +1 @@
export const title = 'IT4C | Error'

View File

@ -1,49 +1,74 @@
import { VueWrapper, mount } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach } from 'vitest'
import { ComponentPublicInstance } from 'vue'
import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import ErrorPage from './+Page.vue'
import { title } from './+title'
describe('ErrorPage', () => {
let wrapper: VueWrapper<unknown, ComponentPublicInstance<unknown, Omit<unknown, never>>>
const Wrapper = () => {
return mount(ErrorPage)
}
beforeEach(() => {
wrapper = Wrapper()
it('title returns correct title', () => {
expect(title).toBe('DreamMall | Fehler')
})
describe('500 Error', () => {
const WrapperUndefined = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component),
},
})
}
const WrapperFalse = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component, {
is404: false,
}),
},
})
}
describe('no is404 property set', () => {
it('renders error 500', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.500.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.500.text')")
let wrapper: ReturnType<typeof WrapperUndefined>
beforeEach(() => {
wrapper = WrapperUndefined()
})
})
describe('is404 property is false', () => {
beforeEach(async () => {
await wrapper.setProps({
is404: false,
describe('no is404 property set', () => {
it('renders error 500', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.500.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.500.text')")
})
})
it('renders error 500', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.500.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.500.text')")
})
})
describe('is404 property is false', () => {
beforeEach(() => {
wrapper = WrapperFalse()
})
describe('is404 property is true', () => {
beforeEach(async () => {
await wrapper.setProps({
is404: true,
it('renders error 500', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.500.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.500.text')")
})
})
it('renders error 400', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.404.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.404.text')")
})
describe('404 Error', () => {
const Wrapper = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component, {
is404: true,
}),
},
})
}
let wrapper: ReturnType<typeof Wrapper>
beforeEach(() => {
wrapper = Wrapper()
})
describe('is404 property is true', () => {
it('renders error 400', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.404.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.404.text')")
})
})
})
})

View File

@ -0,0 +1 @@
export const title = 'IT4C | About'

View File

@ -4,6 +4,7 @@ import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import AboutPage from './+Page.vue'
import { title } from './+title'
describe('AboutPage', () => {
const wrapper = mount(VApp, {
@ -12,6 +13,10 @@ describe('AboutPage', () => {
},
})
it('title returns correct title', () => {
expect(title).toBe('DreamMall')
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})

View File

@ -1,6 +1,5 @@
import { resolveRoute } from 'vike/routing'
import { PageContext } from '#types/PageContext'
import { PageContext } from 'vike/types'
export default (pageContext: PageContext) => {
{

1
src/pages/app/+title.ts Normal file
View File

@ -0,0 +1 @@
export const title = 'IT4C | App'

View File

@ -4,6 +4,7 @@ import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import AppPage from './+Page.vue'
import { title } from './+title'
describe('AppPage', () => {
const wrapper = mount(VApp, {
@ -12,6 +13,10 @@ describe('AppPage', () => {
},
})
it('title returns correct title', () => {
expect(title).toBe('DreamMall')
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})

View File

@ -0,0 +1,3 @@
import { META } from '#src/env'
export const title = META.DEFAULT_TITLE

View File

@ -4,14 +4,19 @@ import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import IndexPage from './+Page.vue'
import { title } from './+title'
describe('DataPrivacyPage', () => {
describe('IndexPage', () => {
const wrapper = mount(VApp, {
slots: {
default: h(IndexPage as Component),
},
})
it('title returns default title', () => {
expect(title).toBe('DreamMall')
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})

16
types/PageContext.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
import { Page } from '#types/Page'
import { PageProps } from '#types/PageProps'
declare global {
namespace Vike {
interface PageContext {
urlPathname: string
config: {
title: string | ((pageContext: PageContext) => string) | undefined
description: string | ((pageContext: PageContext) => string) | undefined
}
Page: Page
pageProps?: PageProps
}
}
}

View File

@ -1,33 +0,0 @@
import { Page } from '#types/Page'
import { PageProps } from '#types/PageProps'
export type {
PageContextServer,
// When using Client Routing https://vike.dev/clientRouting
PageContextClient,
PageContext as VikePageContext,
PageContextBuiltInClientWithClientRouting as PageContextBuiltInClient,
// When using Server Routing
/*
PageContextClientWithServerRouting as PageContextClient,
PageContextWithServerRouting as PageContext,
*/
//* /
} from 'vike/types'
export type PageContext = {
Page: Page
pageProps?: PageProps
urlPathname: string
exports: {
documentProps?: {
title?: string
description?: string
}
}
documentProps?: {
title: string
description?: string
}
}