mirror of
https://github.com/IT4Change/boilerplate-frontend.git
synced 2025-12-12 15:15:49 +00:00
per-page-title
This commit is contained in:
parent
335de9753b
commit
5fd3b4f7ad
@ -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"
|
||||
@ -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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 || {})
|
||||
|
||||
@ -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
19
renderer/utils.ts
Normal 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 }
|
||||
@ -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
src/env.ts
10
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 }
|
||||
|
||||
1
src/pages/_error/+title.ts
Normal file
1
src/pages/_error/+title.ts
Normal file
@ -0,0 +1 @@
|
||||
export const title = 'IT4C | Error'
|
||||
@ -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')")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
1
src/pages/about/+title.ts
Normal file
1
src/pages/about/+title.ts
Normal file
@ -0,0 +1 @@
|
||||
export const title = 'IT4C | About'
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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
1
src/pages/app/+title.ts
Normal file
@ -0,0 +1 @@
|
||||
export const title = 'IT4C | App'
|
||||
@ -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()
|
||||
})
|
||||
|
||||
3
src/pages/index/+title.ts
Normal file
3
src/pages/index/+title.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { META } from '#src/env'
|
||||
|
||||
export const title = META.DEFAULT_TITLE
|
||||
@ -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
16
types/PageContext.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user