Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Import all missing front-end roadiz dependency files #18

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 { NSMenu, NSPage } from '~/types/roadiz'
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
Loading