Skip to content

Commit

Permalink
feat: Import all missing front-end roadiz dependency files
Browse files Browse the repository at this point in the history
  • Loading branch information
willybrauner committed May 6, 2024
1 parent 90a97aa commit 577a846
Show file tree
Hide file tree
Showing 16 changed files with 473 additions and 3 deletions.
17 changes: 17 additions & 0 deletions components/organisms/VDefaultPage/VDefaultPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import type { RoadizNodesSources, RoadizWalker } from '@roadiz/types'
defineProps<{
blocks: RoadizWalker[]
entity: RoadizNodesSources
}>()
</script>

<template>
<div :class="$style.root">{{ entity.title || 'VDefaultPage' }}</div>
</template>

<style lang="scss" module>
.root {
}
</style>
26 changes: 26 additions & 0 deletions composables/use-alternate-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { RoadizAlternateLink } from '@roadiz/types'
import type { LocaleObject } from '@nuxtjs/i18n'

export function useAlternateLinks(links?: RoadizAlternateLink[]) {
const alternateLinks = useState<RoadizAlternateLink[]>('alternateLinks', () => [])

if (links) alternateLinks.value = links

const { $i18n } = useNuxtApp()

const availableAlternateLinks = computed(() => {
const locales =
($i18n.locales.value?.some((locale: unknown) => typeof locale === 'string')
? ($i18n.locales.value as unknown as string[])
: ($i18n.locales.value as LocaleObject[]).map((locale) => locale.code)) || []

return alternateLinks.value.sort((a: RoadizAlternateLink, b: RoadizAlternateLink) => {
const indexA = locales.includes(a.locale) ? locales.indexOf(a.locale) : 9999
const indexB = locales.includes(b.locale) ? locales.indexOf(b.locale) : 9999

return indexA - indexB
})
})

return { alternateLinks, availableAlternateLinks }
}
25 changes: 25 additions & 0 deletions composables/use-common-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { CommonContent, CommonContentMenuKey } from '~/types/api'
import type { ValueOf } from '~/utils/types'

export const COMMON_CONTENT_KEY = 'commonContent'

export function useCommonContent() {
const commonContent = useNuxtData<CommonContent>(COMMON_CONTENT_KEY).data
const homeItem = computed(() => commonContent.value?.home)
const head = computed(() => commonContent.value?.head)
const errorWalker = computed(() => commonContent.value?.errorPage)

function getMenu(key: CommonContentMenuKey): ValueOf<CommonContent['menus']> | undefined {
return commonContent.value?.menus?.[key]
}

const mainMenuWalker = computed(() => getMenu('mainMenuWalker'))

return {
commonContent,
head,
homeItem,
mainMenuWalker,
errorWalker,
}
}
5 changes: 5 additions & 0 deletions composables/use-current-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Page } from '~/composables/use-page'

export function useCurrentPage() {
return useState<Page>('currentPage', () => ({}))
}
14 changes: 11 additions & 3 deletions pages/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@
// })
import type { RoadizNodesSources } from '@roadiz/types'
import { getBlockCollection } from '~/utils/roadiz/block'
import { isPageEntity } from '~/utils/roadiz/entity'
const { item, error } = await useRoadizWebResponse<RoadizNodesSources>()
const { webResponse, item, error } = await useRoadizWebResponse<RoadizNodesSources>()
if (error) {
showError(error)
}
const route = useRoute()
// Force redirect when web response URL is not matching current route path
if (item?.url && item.url !== route.path) {
await navigateTo({ path: item?.url }, { redirectCode: 301 })
}
// Get blocks from web response
const blocks = computed(() => (webResponse?.blocks && getBlockCollection(webResponse.blocks)) || [])
// Get default page entity
const defaultPageEntity = computed(() => item && isPageEntity(item) && item)
</script>

<template>
<div>Hello world</div>
<LazyVDefaultPage v-if="defaultPageEntity" :blocks="blocks" :entity="defaultPageEntity" />
</template>

<style module lang="scss"></style>
141 changes: 141 additions & 0 deletions plugins/01.init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { joinURL } from 'ufo'
import type { RoadizAlternateLink, RoadizNodesSources, RoadizWebResponse } from '@roadiz/types'
import type { Link, Script } from '@unhead/schema'
import { COMMON_CONTENT_KEY, useCommonContent } from '~/composables/use-common-content'
import { createGoogleTagManagerScript } from '~/utils/tracking/google-tag-manager'
import { createMatomoTagManagerScript } from '~/utils/tracking/matomo-tag-manager'

async function initI18n(locale?: string) {
const nuxtApp = useNuxtApp()

if (locale) {
const { $i18n } = nuxtApp
await $i18n.setLocale(locale)
} else {
// get the locale from the route (prefix) or cookie ?
}
}

async function initCommonContent() {
await useRoadizFetch('/common_content', {
key: COMMON_CONTENT_KEY,
})
}

function initHead(webResponse?: RoadizWebResponse, alternateLinks?: RoadizAlternateLink[]) {
const nuxtApp = useNuxtApp()
const route = useRoute()
const runtimeConfig = useRuntimeConfig()
const { $i18n } = nuxtApp
const script: (Script<['script']> | string)[] = []
const link: Link[] = [
{
rel: 'canonical',
href: joinURL(runtimeConfig.public.site.url, webResponse?.item?.url || route.path),
},
]

// ALTERNATE LINKS
const alternateLinksHead = alternateLinks?.map((alternateLink: RoadizAlternateLink) => {
return {
hid: `alternate-${alternateLink.locale}`,
rel: 'alternate',
hreflang: alternateLink.locale,
href: joinURL(runtimeConfig.public.site.url, alternateLink.url),
}
})
if (alternateLinksHead) link.push(...alternateLinksHead)

// GOOGLE TAG MANAGER
// Google Tag Manager must not be loaded by tarteaucitron, it must configure tarteaucitron itself.
// Notice: by using GTM you must comply with GDPR and cookie consent or just use
// tarteaucitron with GA4, Matomo or Plausible
const googleTagManager = runtimeConfig.public.googleTagManager
if (googleTagManager && googleTagManager !== '') {
script.push(createGoogleTagManagerScript(googleTagManager))
}

// MATOMO
const matomoURL = runtimeConfig.public.matomo?.url
const matomoContainerID = runtimeConfig.public.matomo?.containerID
if (matomoURL && matomoContainerID && matomoURL !== '' && matomoContainerID !== '') {
script.push(createMatomoTagManagerScript(matomoContainerID, matomoURL))
}

useHead({
htmlAttrs: {
lang: $i18n.locale.value,
},
script,
link,
meta: [
// app version
{ name: 'version', content: runtimeConfig.public.version },
],
})
}

function initSeoMeta(webResponse?: RoadizWebResponse) {
const nuxtApp = useNuxtApp()
const { commonContent } = useCommonContent()
const runtimeConfig = useRuntimeConfig()
const head = webResponse?.head
const description = webResponse?.head?.metaDescription || commonContent.value?.head?.metaDescription
const title = webResponse?.head?.metaTitle || commonContent.value?.head?.metaTitle
const siteName = commonContent.value?.head?.siteName || (nuxtApp.$config.siteName as string) || ''
const { isActive: previewIsActive } = useRoadizPreview()
const img = useImage()
const image = () => {
const image =
head?.shareImage?.relativePath ||
// @ts-ignore not sure the `images` property exists, but generally it does
head?.images?.[0]?.relativePath ||
// @ts-ignore not sure the `image` property exists, but generally it does
head?.image?.[0]?.relativePath ||
commonContent.value?.head?.shareImage?.relativePath

if (image) {
return img(image, {
width: 1200,
quality: 70,
})
} else {
return joinURL(runtimeConfig.public.site.url, '/images/share.jpg')
}
}

useServerSeoMeta({
description,
ogTitle: title,
ogSiteName: siteName,
ogDescription: description,
ogImage: image(),
twitterCard: 'summary',
twitterTitle: title,
twitterDescription: description,
robots: {
noindex: (webResponse?.item as RoadizNodesSources)?.noIndex || previewIsActive.value,
},
})
}

export default defineNuxtPlugin(async () => {
const route = useRoute()
const isWildCardRoute = route.name === 'slug'
const data = isWildCardRoute ? await useRoadizWebResponse() : undefined
const locale = data && ((data.item as RoadizNodesSources)?.translation?.locale || undefined)

if (locale) {
// Set currentPage data accessible outside pages
useCurrentPage().value = {
webResponse: data.webResponse,
alternateLinks: data.alternateLinks,
}

await initI18n(locale)
useAlternateLinks(data?.alternateLinks)
}
await initCommonContent()
initHead(data?.webResponse, data?.alternateLinks)
initSeoMeta(data?.webResponse)
})
47 changes: 47 additions & 0 deletions types/api.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { RoadizAlternateLink, RoadizNodesSources, RoadizNodesSourcesHead, RoadizWebResponse } from '@roadiz/types'
import type { MenuNodeType } from '~/types/app'
import type { NSFooter, NSMenu, NSPage } from '~/types/roadiz'

Check warning on line 3 in types/api.d.ts

View workflow job for this annotation

GitHub Actions / lint

'NSFooter' is defined but never used
import type { RoadizWalkerKnown } from '~/utils/types'

interface HydraError {
'@context': string
'@type': string
'hydra:title': string
'hydra:description': string
message?: string
trace: Array<HydraErrorTraceItem>
}

interface HydraErrorTraceItem {
file: string
line: number
function: string
class: string
type: string
args: Array<string>
}

interface PageResponse {
webResponse: RoadizWebResponse | undefined
alternateLinks?: RoadizAlternateLink[]
locale?: string
}

type CommonContentMenuKey = 'mainMenuWalker' | 'footerMenuWalker' | 'headerMenuWalker' | string

interface CommonContent {
home?: RoadizNodesSources
head?: RoadizNodesSourcesHead
menus?: Record<CommonContentMenuKey, RoadizWalkerKnown<NSMenu, MenuNodeType>>
errorPage?: RoadizWalkerKnown<NSPage>
}

interface CustomForm extends JsonLdObject {
slug?: boolean
name?: boolean
open?: boolean
definitionUrl?: string | null
postUrl?: string | null
description?: string | null
color?: string | null
}
8 changes: 8 additions & 0 deletions utils/roadiz/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { RoadizWalker, RoadizWebResponseBlocks } from '@roadiz/types'
import { getArrayFromCollection } from '~/utils/roadiz/get-array-from-collection'

// we have to get a hydra collection of RoadizWalker (JSON LD format),
// because if the API has been requested as JSON format then we won't be able to parse the result (lack of '@type' properties)
export function getBlockCollection(blocks: RoadizWebResponseBlocks): RoadizWalker[] {
return getArrayFromCollection<RoadizWalker>(blocks)
}
35 changes: 35 additions & 0 deletions utils/roadiz/entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { JsonLdObject } from '@roadiz/types'

export function isNodeType(entity: unknown): entity is JsonLdObject {
return !!(entity && typeof entity === 'object' && '@id' in entity && '@type' in entity)
}

export function isEntityType(entity: JsonLdObject, type: string): boolean {
const regex = new RegExp('^(NS)?' + type + '$', 'gi')
const matches = entity['@type']?.match(regex)
return matches !== null && matches.length > 0
}

export function isSchemaOrgType(entity: JsonLdObject, type: string): boolean {
const regex = new RegExp('^(?:https?:\\/\\/schema\\.org\\/)?' + type + '$', 'gi')
const matches = entity['@type']?.match(regex)
return matches !== null && matches.length > 0
}

export function isPageEntity(entity: JsonLdObject): boolean {
return isEntityType(entity, 'Page')
}

export function isBlogPostEntity(entity: JsonLdObject): boolean {
return isEntityType(entity, 'BlogPost')
}

export function isBlogListingEntity(entity: JsonLdObject): boolean {
return isEntityType(entity, 'BlogPostContainer')
}

// BLOCKS

export function isContentBlock(entity: JsonLdObject): boolean {
return isEntityType(entity, 'ContentBlock')
}
9 changes: 9 additions & 0 deletions utils/roadiz/get-array-from-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { HydraCollection, JsonLdObject } from '@roadiz/types'

export function getArrayFromCollection<T>(
collection: HydraCollection<T> | Array<Omit<T, keyof JsonLdObject>> | unknown[],
): T[] {
if (Array.isArray(collection)) return collection as T[]

return 'hydra:member' in collection && Array.isArray(collection['hydra:member']) ? collection['hydra:member'] : []
}
13 changes: 13 additions & 0 deletions utils/tracking/google-analytics-4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @deprecated Use tarteaucitron for better GDPR integration
* @see https://developers.google.com/analytics/devguides/collection/gtagjs
* @param id
*/
export function createGoogleAnalytics4Script(id: string): string {
return `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${id}');
`
}
13 changes: 13 additions & 0 deletions utils/tracking/google-tag-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @see https://developers.google.com/tag-platform/tag-manager/web
* @param id
*/
export function createGoogleTagManagerScript(id: string): string {
return `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${id}');
`
}
13 changes: 13 additions & 0 deletions utils/tracking/matomo-tag-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @see https://developer.matomo.org/guides/tagmanager/embedding
* @param {string} id
* @param {string} matomoTagManagerUrl
*/
export function createMatomoTagManagerScript(id: string, matomoTagManagerUrl: string): string {
return `
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='${matomoTagManagerUrl}/js/container_${id}.js'; s.parentNode.insertBefore(g,s);
`
}
Loading

0 comments on commit 577a846

Please sign in to comment.