Skip to content

Commit

Permalink
Merge pull request #69 from IT4Change/per-page-title
Browse files Browse the repository at this point in the history
feat(frontend): per page title
  • Loading branch information
ulfgebhardt authored Jan 28, 2024
2 parents 335de97 + 010fbb8 commit 297e05a
Show file tree
Hide file tree
Showing 21 changed files with 172 additions and 91 deletions.
6 changes: 4 additions & 2 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions renderer/+config.h.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
},
}
9 changes: 6 additions & 3 deletions renderer/+onRenderClient.ts
Original file line number Diff line number Diff line change
@@ -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
27 changes: 22 additions & 5 deletions renderer/+onRenderHtml.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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 { getDescription, getTitle } from './utils'

import type { PageContextServer, PageContext } from '#types/PageContext'
import type { App } from 'vue'

async function render(pageContext: PageContextServer & PageContext) {
Expand All @@ -17,17 +19,32 @@ 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}">
<head>
<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>
Expand Down
7 changes: 3 additions & 4 deletions renderer/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { PageContext } from 'vike/types'
import { createSSRApp, defineComponent, h, markRaw, reactive, Component } from 'vue'

import PageShell from '#components/PageShell.vue'
Expand All @@ -7,11 +8,9 @@ import i18n from '#plugins/i18n'
import pinia from '#plugins/pinia'
import CreateVuetify from '#plugins/vuetify'

import type { PageContext, VikePageContext } from '#types/PageContext'

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({
Expand Down Expand Up @@ -47,7 +46,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 || {})
Expand Down
7 changes: 3 additions & 4 deletions renderer/context/usePageContext.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
// `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)
if (!pageContext) throw new Error('setPageContext() not called in parent')
return pageContext
}

function setPageContext(app: App, pageContext: VikePageContext & PageContext) {
function setPageContext(app: App, pageContext: PageContext) {
app.provide(vikePageContext, pageContext)
}

Expand Down
19 changes: 19 additions & 0 deletions renderer/utils.ts
Original file line number Diff line number Diff line change
@@ -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 }
4 changes: 3 additions & 1 deletion src/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
})
})
10 changes: 6 additions & 4 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -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 }
1 change: 1 addition & 0 deletions src/pages/_error/+title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const title = 'IT4C | Error'
87 changes: 56 additions & 31 deletions src/pages/_error/Page.test.ts
Original file line number Diff line number Diff line change
@@ -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('IT4C | Error')
})
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()
})

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 true', () => {
beforeEach(async () => {
await wrapper.setProps({
is404: true,
describe('404 Error', () => {
const Wrapper = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component, {
is404: true,
}),
},
})
}
let wrapper: ReturnType<typeof Wrapper>
beforeEach(() => {
wrapper = Wrapper()
})

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('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')")
})
})
})
})
1 change: 1 addition & 0 deletions src/pages/about/+title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const title = 'IT4C | About'
5 changes: 5 additions & 0 deletions src/pages/about/Page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -12,6 +13,10 @@ describe('AboutPage', () => {
},
})

it('title returns correct title', () => {
expect(title).toBe('IT4C | About')
})

it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
Expand Down
3 changes: 1 addition & 2 deletions src/pages/app/+route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { resolveRoute } from 'vike/routing'

import { PageContext } from '#types/PageContext'
import { PageContext } from 'vike/types'

export default (pageContext: PageContext) => {
{
Expand Down
1 change: 1 addition & 0 deletions src/pages/app/+title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const title = 'IT4C | App'
5 changes: 5 additions & 0 deletions src/pages/app/Page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -12,6 +13,10 @@ describe('AppPage', () => {
},
})

it('title returns correct title', () => {
expect(title).toBe('IT4C | App')
})

it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
Expand Down
3 changes: 3 additions & 0 deletions src/pages/index/+title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { META } from '#src/env'

export const title = META.DEFAULT_TITLE
7 changes: 6 additions & 1 deletion src/pages/index/Page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('IT4C')
})

it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
Expand Down
2 changes: 1 addition & 1 deletion src/pages/index/__snapshots__/Page.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`DataPrivacyPage > renders 1`] = `
exports[`IndexPage > renders 1`] = `
<div
class="v-application v-theme--light v-layout v-layout--full-height v-locale--is-ltr"
>
Expand Down
Loading

0 comments on commit 297e05a

Please sign in to comment.