From 5fd3b4f7ada99770ddbd5edd7ecf04e8528545dc Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sun, 28 Jan 2024 12:25:43 +0100 Subject: [PATCH] per-page-title --- .env.dist | 6 +- renderer/+config.h.ts | 10 ++++ renderer/+onRenderClient.ts | 9 ++- renderer/+onRenderHtml.ts | 30 ++++++++-- renderer/app.ts | 6 +- renderer/context/usePageContext.ts | 7 +-- renderer/utils.ts | 19 +++++++ src/env.test.ts | 4 +- src/env.ts | 10 ++-- src/pages/_error/+title.ts | 1 + src/pages/_error/Page.test.ts | 89 +++++++++++++++++++----------- src/pages/about/+title.ts | 1 + src/pages/about/Page.test.ts | 5 ++ src/pages/app/+route.ts | 3 +- src/pages/app/+title.ts | 1 + src/pages/app/Page.test.ts | 5 ++ src/pages/index/+title.ts | 3 + src/pages/index/Page.test.ts | 7 ++- types/PageContext.d.ts | 16 ++++++ types/PageContext.ts | 33 ----------- 20 files changed, 174 insertions(+), 91 deletions(-) create mode 100644 renderer/utils.ts create mode 100644 src/pages/_error/+title.ts create mode 100644 src/pages/about/+title.ts create mode 100644 src/pages/app/+title.ts create mode 100644 src/pages/index/+title.ts create mode 100644 types/PageContext.d.ts delete mode 100644 types/PageContext.ts diff --git a/.env.dist b/.env.dist index 0db1427..6c87f75 100644 --- a/.env.dist +++ b/.env.dist @@ -1,3 +1,5 @@ # META -PUBLIC_ENV__META__DEFAULT_TITLE="IT4C" -PUBLIC_ENV__META__DEFAULT_DESCRIPTION="IT4C Frontend Boilerplate" \ No newline at end of file +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" \ No newline at end of file diff --git a/renderer/+config.h.ts b/renderer/+config.h.ts index 5e4c765..ceabba4 100644 --- a/renderer/+config.h.ts +++ b/renderer/+config.h.ts @@ -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 }, + }, + }, } diff --git a/renderer/+onRenderClient.ts b/renderer/+onRenderClient.ts index 825c8c1..bf3c357 100644 --- a/renderer/+onRenderClient.ts +++ b/renderer/+onRenderClient.ts @@ -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 -/* 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 diff --git a/renderer/+onRenderHtml.ts b/renderer/+onRenderHtml.ts index 6b95973..4de2ddb 100644 --- a/renderer/+onRenderHtml.ts +++ b/renderer/+onRenderHtml.ts @@ -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` @@ -27,7 +29,23 @@ async function render(pageContext: PageContextServer & PageContext) { - + + + + + + + + + + + + + + + + + ${title} diff --git a/renderer/app.ts b/renderer/app.ts index 341a63c..2c00c0a 100644 --- a/renderer/app.ts +++ b/renderer/app.ts @@ -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 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 || {}) diff --git a/renderer/context/usePageContext.ts b/renderer/context/usePageContext.ts index 103e2ba..abcb2fa 100644 --- a/renderer/context/usePageContext.ts +++ b/renderer/context/usePageContext.ts @@ -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 = Symbol('pageContext') +export const vikePageContext: InjectionKey = 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) } diff --git a/renderer/utils.ts b/renderer/utils.ts new file mode 100644 index 0000000..72833d5 --- /dev/null +++ b/renderer/utils.ts @@ -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 } diff --git a/src/env.test.ts b/src/env.test.ts index a46f9c2..8055cfd 100644 --- a/src/env.test.ts +++ b/src/env.test.ts @@ -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', }) }) }) diff --git a/src/env.ts b/src/env.ts index 5eccab8..b9dbdb1 100644 --- a/src/env.ts +++ b/src/env.ts @@ -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 } diff --git a/src/pages/_error/+title.ts b/src/pages/_error/+title.ts new file mode 100644 index 0000000..19ee0b5 --- /dev/null +++ b/src/pages/_error/+title.ts @@ -0,0 +1 @@ +export const title = 'IT4C | Error' diff --git a/src/pages/_error/Page.test.ts b/src/pages/_error/Page.test.ts index 2ec5a0a..a7c4bca 100644 --- a/src/pages/_error/Page.test.ts +++ b/src/pages/_error/Page.test.ts @@ -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>> - 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 + 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 + 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')") + }) }) }) }) diff --git a/src/pages/about/+title.ts b/src/pages/about/+title.ts new file mode 100644 index 0000000..1fdc696 --- /dev/null +++ b/src/pages/about/+title.ts @@ -0,0 +1 @@ +export const title = 'IT4C | About' diff --git a/src/pages/about/Page.test.ts b/src/pages/about/Page.test.ts index cdf7d56..5bb7b6c 100644 --- a/src/pages/about/Page.test.ts +++ b/src/pages/about/Page.test.ts @@ -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() }) diff --git a/src/pages/app/+route.ts b/src/pages/app/+route.ts index a73deee..1fcedec 100644 --- a/src/pages/app/+route.ts +++ b/src/pages/app/+route.ts @@ -1,6 +1,5 @@ import { resolveRoute } from 'vike/routing' - -import { PageContext } from '#types/PageContext' +import { PageContext } from 'vike/types' export default (pageContext: PageContext) => { { diff --git a/src/pages/app/+title.ts b/src/pages/app/+title.ts new file mode 100644 index 0000000..9c85bec --- /dev/null +++ b/src/pages/app/+title.ts @@ -0,0 +1 @@ +export const title = 'IT4C | App' diff --git a/src/pages/app/Page.test.ts b/src/pages/app/Page.test.ts index 14ce298..9c7b6b0 100644 --- a/src/pages/app/Page.test.ts +++ b/src/pages/app/Page.test.ts @@ -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() }) diff --git a/src/pages/index/+title.ts b/src/pages/index/+title.ts new file mode 100644 index 0000000..125d4b6 --- /dev/null +++ b/src/pages/index/+title.ts @@ -0,0 +1,3 @@ +import { META } from '#src/env' + +export const title = META.DEFAULT_TITLE diff --git a/src/pages/index/Page.test.ts b/src/pages/index/Page.test.ts index 8596ddd..026c20f 100644 --- a/src/pages/index/Page.test.ts +++ b/src/pages/index/Page.test.ts @@ -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() }) diff --git a/types/PageContext.d.ts b/types/PageContext.d.ts new file mode 100644 index 0000000..0b93d96 --- /dev/null +++ b/types/PageContext.d.ts @@ -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 + } + } +} diff --git a/types/PageContext.ts b/types/PageContext.ts deleted file mode 100644 index 86a8da4..0000000 --- a/types/PageContext.ts +++ /dev/null @@ -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 - } -}