@live-change/content-frontend 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/front/locales/en.json +84 -0
- package/front/src/App.vue +4 -1
- package/front/src/components/Metadata.vue +134 -0
- package/front/src/components/MetadataEditor.vue +40 -11
- package/front/src/components/Page.vue +5 -1
- package/front/src/components/PageEditor.vue +3 -2
- package/front/src/config.js +13 -0
- package/front/src/entry-client.js +2 -2
- package/front/src/entry-server.js +2 -1
- package/package.json +6 -5
- package/server/init.js +34 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"content": {
|
|
3
|
+
"metadata": {
|
|
4
|
+
"title": "Title",
|
|
5
|
+
"description": "Description",
|
|
6
|
+
"og:title": "Open Graph",
|
|
7
|
+
"og": {
|
|
8
|
+
"title": "Title",
|
|
9
|
+
"description": "Description",
|
|
10
|
+
"image": "Image",
|
|
11
|
+
"determiner": "Determiner",
|
|
12
|
+
"locale": "Locale",
|
|
13
|
+
"localeAlternate:title": "Locale Alternate",
|
|
14
|
+
"type": "Type",
|
|
15
|
+
"type:options": {
|
|
16
|
+
"website": "Website",
|
|
17
|
+
"article": "Article",
|
|
18
|
+
"profile": "Profile",
|
|
19
|
+
"book": "Book",
|
|
20
|
+
"music": {
|
|
21
|
+
"song": "Music Song",
|
|
22
|
+
"album": "Music Album",
|
|
23
|
+
"playlist": "Music Playlist",
|
|
24
|
+
"radio_station": "Music Radio Station"
|
|
25
|
+
},
|
|
26
|
+
"video": {
|
|
27
|
+
"movie": "Video Movie",
|
|
28
|
+
"episode": "Video Episode",
|
|
29
|
+
"tv_show": "Video TV Show",
|
|
30
|
+
"other": "Video Other"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"music:title": "Music",
|
|
34
|
+
"music": {
|
|
35
|
+
"duration": "Duration",
|
|
36
|
+
"song:title": "Song",
|
|
37
|
+
"creator:title": "Creator",
|
|
38
|
+
"album:title": "Album",
|
|
39
|
+
"album": {
|
|
40
|
+
"url": "URL",
|
|
41
|
+
"disc": "Disc",
|
|
42
|
+
"track": "Track"
|
|
43
|
+
},
|
|
44
|
+
"song": {
|
|
45
|
+
"url": "URL",
|
|
46
|
+
"disc": "Disc",
|
|
47
|
+
"track": "Track"
|
|
48
|
+
},
|
|
49
|
+
"musician:title": "Musician"
|
|
50
|
+
},
|
|
51
|
+
"video:title": "Video",
|
|
52
|
+
"video": {
|
|
53
|
+
"duration": "Duration",
|
|
54
|
+
"actor:title": "Actor",
|
|
55
|
+
"actor": {
|
|
56
|
+
"url": "URL",
|
|
57
|
+
"role": "Role"
|
|
58
|
+
},
|
|
59
|
+
"director:title": "Director",
|
|
60
|
+
"writer:title": "Writer",
|
|
61
|
+
"releaseDate": "Release Date",
|
|
62
|
+
"tag:title": "Tag",
|
|
63
|
+
"series": "Series"
|
|
64
|
+
},
|
|
65
|
+
"article:title": "Article",
|
|
66
|
+
"article": {
|
|
67
|
+
"publishedTime": "Published Time",
|
|
68
|
+
"modifiedTime": "Modified Time",
|
|
69
|
+
"expirationTime": "Expiration Time",
|
|
70
|
+
"author:title": "Authors",
|
|
71
|
+
"section": "Section",
|
|
72
|
+
"tag:title": "Tags"
|
|
73
|
+
},
|
|
74
|
+
"book:title": "Book",
|
|
75
|
+
"book": {
|
|
76
|
+
"author:title": "Authors",
|
|
77
|
+
"isbn": "ISBN",
|
|
78
|
+
"releaseDate": "Release Date",
|
|
79
|
+
"tag:title": "Tags"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/front/src/App.vue
CHANGED
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
import ViewRoot from "@live-change/frontend-base/ViewRoot.vue"
|
|
14
14
|
import NavBar from "./NavBar.vue"
|
|
15
15
|
|
|
16
|
+
import { useI18n } from 'vue-i18n'
|
|
17
|
+
const i18n = useI18n()
|
|
18
|
+
|
|
16
19
|
import { useMeta } from 'vue-meta'
|
|
17
20
|
const { meta } = useMeta({
|
|
18
21
|
title: 'Title',
|
|
19
22
|
htmlAttrs: {
|
|
20
|
-
lang:
|
|
23
|
+
lang: i18n.locale.value,
|
|
21
24
|
amp: true
|
|
22
25
|
}
|
|
23
26
|
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- <pre>{{ JSON.stringify(metadata, null, " ") }}</pre>-->
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
objectType: {
|
|
8
|
+
type: String,
|
|
9
|
+
required: true
|
|
10
|
+
},
|
|
11
|
+
object: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
url: {
|
|
16
|
+
type: Object,
|
|
17
|
+
required: true
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
import { computed, ref, onMounted } from 'vue'
|
|
22
|
+
import { path, live } from '@live-change/vue3-ssr'
|
|
23
|
+
import { useHost } from "@live-change/frontend-base"
|
|
24
|
+
const host = useHost()
|
|
25
|
+
|
|
26
|
+
const p = path()
|
|
27
|
+
|
|
28
|
+
const metadataLivePath = computed(
|
|
29
|
+
() => p.content.objectOwnedMetadata({ objectType: props.objectType, object: props.object })
|
|
30
|
+
.with(metadata => p.image.image({ image: metadata.og.image }).bind('ogImage'))
|
|
31
|
+
)
|
|
32
|
+
const canonicalUrlLivePath = computed(
|
|
33
|
+
() => p.url.targetOwnedCanonical({ targetType: props.objectType, target: props.object })
|
|
34
|
+
)
|
|
35
|
+
const [metadata, canonical] = await Promise.all([
|
|
36
|
+
live(metadataLivePath),
|
|
37
|
+
live(canonicalUrlLivePath)
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
function metaProperty(name, value) {
|
|
41
|
+
return value ? { property: name, content: value } : undefined
|
|
42
|
+
}
|
|
43
|
+
function metaProperties(name, value) {
|
|
44
|
+
if(!value) return []
|
|
45
|
+
return value.map(v => ({ property: name, content: v }))
|
|
46
|
+
}
|
|
47
|
+
function metaPropertiesObjects(name, value) {
|
|
48
|
+
if(!value) return []
|
|
49
|
+
return value.map(v => {
|
|
50
|
+
for(let key in v) {
|
|
51
|
+
return { property: name+':'+key, content: v[key] }
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
import { useMeta } from 'vue-meta'
|
|
56
|
+
const m = metadata.value
|
|
57
|
+
const canonicalUrlDomain = canonical.value?.domain || host
|
|
58
|
+
const canonicalUrl = `https://${canonicalUrlDomain}/${canonical.value?.path ?? ''}`
|
|
59
|
+
|
|
60
|
+
let ogImage = []
|
|
61
|
+
if(m.ogImage) {
|
|
62
|
+
const image = m.ogImage
|
|
63
|
+
ogImage = [
|
|
64
|
+
{ property: 'og:image', content: `https://${canonicalUrlDomain}/api/image/image/${image.id}` },
|
|
65
|
+
{ property: 'og:image:width', content: image.width },
|
|
66
|
+
{ property: 'og:image:height', content: image.height },
|
|
67
|
+
{ property: 'og:image:type', content: 'image/'+image.extension }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
useMeta({
|
|
72
|
+
title: m.title,
|
|
73
|
+
description: m.description,
|
|
74
|
+
link: [
|
|
75
|
+
{ rel: 'canonical', href: canonicalUrl }
|
|
76
|
+
],
|
|
77
|
+
meta: [
|
|
78
|
+
metaProperty('og:title', m.og.title),
|
|
79
|
+
metaProperty('og:description', m.og.description),
|
|
80
|
+
...ogImage,
|
|
81
|
+
metaProperty('og:determiner', m.og.determiner),
|
|
82
|
+
metaProperty('og:locale', m.og.locale),
|
|
83
|
+
...metaProperties('og:locale:alternate', m.og.localeAlternate),
|
|
84
|
+
metaProperty('og:type', m.og.type),
|
|
85
|
+
|
|
86
|
+
metaProperty('og:music:duration', m.og.music.duration),
|
|
87
|
+
...metaPropertiesObjects('og:music:album', m.og.music.album),
|
|
88
|
+
...metaPropertiesObjects('og:music:song', m.og.music.song),
|
|
89
|
+
...metaProperties('og:music:musician', m.og.music.duration),
|
|
90
|
+
metaProperty('og:music:release_date', m.og.music.releaseDate),
|
|
91
|
+
...metaProperties('og:music:creator', m.og.music.creator),
|
|
92
|
+
|
|
93
|
+
metaProperty('og:video:duration', m.og.video.duration),
|
|
94
|
+
metaProperty('og:viceo:release_date', m.og.video.releaseDate),
|
|
95
|
+
...metaPropertiesObjects('og:video:duration', m.og.video.actors),
|
|
96
|
+
...metaProperties('og:video:director', m.og.video.director),
|
|
97
|
+
...metaProperties('og:video:writer', m.og.video.writer),
|
|
98
|
+
...metaProperties('og:video:series', m.og.video.series),
|
|
99
|
+
...metaProperties('og:video:tag', m.og.video.tag),
|
|
100
|
+
|
|
101
|
+
...metaProperties('og:profile:first_name', m.og.profile.firstName),
|
|
102
|
+
...metaProperties('og:profile:last_name', m.og.profile.lastName),
|
|
103
|
+
...metaProperties('og:profile:username', m.og.profile.username),
|
|
104
|
+
...metaProperties('og:profile:gender', m.og.profile.gender),
|
|
105
|
+
|
|
106
|
+
metaProperty('og:article:published_time', m.og.article.publishedTime),
|
|
107
|
+
metaProperty('og:article:modified_time', m.og.article.modifiedTime),
|
|
108
|
+
metaProperty('og:article:expiration_time', m.og.article.expirationTime),
|
|
109
|
+
...metaProperties('og:article:author', m.og.article.author),
|
|
110
|
+
metaProperty('og:article:section', m.og.article.section),
|
|
111
|
+
...metaProperties('og:article:tag', m.og.article.tag),
|
|
112
|
+
|
|
113
|
+
...metaProperties('og:book:author', m.og.book.author),
|
|
114
|
+
metaProperty('og:book:isbn', m.og.book.isbn),
|
|
115
|
+
metaProperty('og:book:release_date', m.og.book.releaseDate),
|
|
116
|
+
...metaProperties('og:book:tag', m.og.book.tag),
|
|
117
|
+
|
|
118
|
+
{ property: 'og:url', content: canonicalUrl },
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/* { property: 'og:url', content: m.url },
|
|
122
|
+
{ property: 'og:type', content: 'website' },
|
|
123
|
+
{ property: 'twitter:card', content: 'summary_large_image' },
|
|
124
|
+
{ property: 'twitter:title', content: metadata.value.title },
|
|
125
|
+
{ property: 'twitter:description', content: metadata.value.description },
|
|
126
|
+
{ property: 'twitter:image', content: metadata.value.image },
|
|
127
|
+
{ property: 'twitter:url', content: metadata.value.url }*/
|
|
128
|
+
].filter(x => !!x)
|
|
129
|
+
})
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<style scoped>
|
|
133
|
+
|
|
134
|
+
</style>
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
2
|
+
<!-- <pre style="white-space: pre-wrap; word-wrap: break-word;">{{ JSON.stringify(editable, null, ' ') }}</pre>-->
|
|
3
|
+
<auto-editor :definition="editableDefinition" v-model="editable" :rootValue="editable" i18n="content.metadata." />
|
|
4
|
+
<Button label="Save metadata" icon="pi pi-save" :disabled="!changed || error" @click="save" />
|
|
5
|
+
<div>
|
|
6
|
+
<small v-if="error" class="p-error">Fix errors above to save</small>
|
|
7
|
+
</div>
|
|
4
8
|
</template>
|
|
5
9
|
|
|
6
10
|
<script setup>
|
|
11
|
+
import Button from 'primevue/button'
|
|
12
|
+
import { AutoInput, AutoField, AutoEditor } from '@live-change/frontend-auto-form'
|
|
7
13
|
|
|
8
|
-
import
|
|
14
|
+
import "@live-change/image-frontend"
|
|
9
15
|
|
|
10
|
-
import { computed, watch, ref, onMounted, inject } from 'vue'
|
|
16
|
+
import { computed, watch, ref, onMounted, onUnmounted, inject } from 'vue'
|
|
11
17
|
import { toRefs } from "@vueuse/core"
|
|
12
18
|
|
|
13
19
|
const isMounted = ref(false)
|
|
@@ -31,11 +37,25 @@
|
|
|
31
37
|
const p = path()
|
|
32
38
|
|
|
33
39
|
const definition = api.getServiceDefinition('content').models.Metadata
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const editableDefinition = {
|
|
41
|
+
...definition,
|
|
42
|
+
properties: { ...{
|
|
43
|
+
...definition.properties,
|
|
44
|
+
objectType: undefined,
|
|
45
|
+
object: undefined,
|
|
46
|
+
lastUpdate: undefined
|
|
47
|
+
} }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
import { useToast } from 'primevue/usetoast'
|
|
51
|
+
const toast = useToast()
|
|
52
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
53
|
+
const confirm = useConfirm()
|
|
54
|
+
|
|
55
|
+
import { synchronized, defaultData, validateData } from "@live-change/vue3-components"
|
|
56
|
+
|
|
57
|
+
const serverMetadata = await live(p.content.objectOwnedMetadata({ objectType, object }))
|
|
58
|
+
const metadata = computed(() => serverMetadata.value || defaultData(editableDefinition))
|
|
39
59
|
|
|
40
60
|
const synchronizedMetadata = synchronized({
|
|
41
61
|
source: metadata,
|
|
@@ -43,13 +63,22 @@
|
|
|
43
63
|
identifiers: { object, objectType },
|
|
44
64
|
recursive: true,
|
|
45
65
|
autoSave: false,
|
|
46
|
-
onSave: () => toast.add({ severity: 'info', summary: '
|
|
47
|
-
})
|
|
66
|
+
onSave: () => toast.add({ severity: 'info', summary: 'Metadata saved', life: 1500 })
|
|
67
|
+
})
|
|
48
68
|
|
|
49
69
|
const editable = synchronizedMetadata.value
|
|
50
70
|
const save = synchronizedMetadata.save
|
|
51
71
|
const changed = synchronizedMetadata.changed
|
|
52
72
|
|
|
73
|
+
const error = computed(() => validateData(editableDefinition, editable.value))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
function beforeUnload(ev) {
|
|
77
|
+
if(changed.value) return ev.returnValue = "You have some unsaved changes!"
|
|
78
|
+
}
|
|
79
|
+
onMounted(() => window.addEventListener('beforeunload', beforeUnload))
|
|
80
|
+
onUnmounted(() => window.removeEventListener('beforeunload', beforeUnload))
|
|
81
|
+
|
|
53
82
|
</script>
|
|
54
83
|
|
|
55
84
|
<style scoped>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<ResolveUrl targetType="content_Page" :path="urlPath" :fetchMore="urlMore">
|
|
3
3
|
<template #default="{ target, style, class: clazz }">
|
|
4
|
+
<Metadata objectType="content_Page" :object="target" />
|
|
4
5
|
<LimitedAccess :requiredRoles="['writer']" objectType="content_Page" :object="target" hidden>
|
|
5
6
|
<PageAdminButtons :page="target" :style="style" :class="clazz" :name="urlPath.value" />
|
|
6
7
|
</LimitedAccess>
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
import { ResolveUrl, NotFound } from "@live-change/url-frontend"
|
|
22
23
|
import { LimitedAccess } from "@live-change/access-control-frontend";
|
|
23
24
|
import Content from "./Content.vue"
|
|
25
|
+
import Metadata from "./Metadata.vue"
|
|
24
26
|
|
|
25
27
|
import { computed, watch, ref, onMounted } from 'vue'
|
|
26
28
|
import { toRefs } from "@vueuse/core"
|
|
@@ -34,7 +36,9 @@
|
|
|
34
36
|
|
|
35
37
|
const urlMore = [
|
|
36
38
|
url => p.content.page({ page: url.target }),
|
|
37
|
-
url => p.content.content({ objectType: 'content_Page', object: url.target })
|
|
39
|
+
url => p.content.content({ objectType: 'content_Page', object: url.target }),
|
|
40
|
+
url => p.content.objectOwnedMetadata({ objectType: 'content_Page', object: url.target }),
|
|
41
|
+
url => p.url.targetOwnedCanonical({ targetType: 'content_Page', target: url.target })
|
|
38
42
|
]
|
|
39
43
|
|
|
40
44
|
const canCreatePage = computed(() => api.client.value.roles.includes('writer'))
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
</AccordionTab>
|
|
15
15
|
<AccordionTab>
|
|
16
16
|
<template #header>
|
|
17
|
-
<span v-if="metadata" class="font-bold mr-1">
|
|
17
|
+
<span v-if="metadata" class="font-bold mr-1">Metadata:</span>
|
|
18
|
+
<span v-if="metadata" class="mr-1 font-normal">{{ metadata.title }}</span>
|
|
18
19
|
<span v-else class="font-bold text-red-600">Metadata not set</span>
|
|
19
20
|
</template>
|
|
20
21
|
<MetadataEditor objectType="content_Page" :object="pageId" :key="pageId"></MetadataEditor>
|
|
21
22
|
</AccordionTab>
|
|
22
23
|
</Accordion>
|
|
23
24
|
|
|
24
|
-
<DocumentEditor v-if="pageData" targetType="content_Page" :target="pageId"
|
|
25
|
+
<DocumentEditor v-if="pageData" targetType="content_Page" :target="pageId" purpose="page"
|
|
25
26
|
:config="contentConfig" type="content" v-model:saveState="saveState" v-model:version="version">
|
|
26
27
|
<template #menuEnd="{}">
|
|
27
28
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import deepmerge from 'deepmerge';
|
|
2
|
+
|
|
3
|
+
import contentEn from "../locales/en.json"
|
|
4
|
+
import { locales as autoFormLocales } from "@live-change/frontend-auto-form"
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
i18nMessages: {
|
|
8
|
+
en: deepmerge.all([
|
|
9
|
+
contentEn,
|
|
10
|
+
autoFormLocales.en
|
|
11
|
+
])
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serverEntry } from '@live-change/frontend-base/server-entry.js'
|
|
2
2
|
import App from './App.vue'
|
|
3
3
|
import { createRouter } from './router'
|
|
4
|
+
import config from './config.js'
|
|
4
5
|
|
|
5
|
-
const render = serverEntry(App, createRouter)
|
|
6
|
+
const render = serverEntry(App, createRouter, config)
|
|
6
7
|
export { render }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/content-frontend",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"memDev": "lcli memDev --enableSessions --initScript ./init.js --dbAccess",
|
|
6
6
|
"localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"debug": "node --inspect-brk server"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@fortawesome/fontawesome-free": "^6.
|
|
23
|
+
"@fortawesome/fontawesome-free": "^6.2.0",
|
|
24
24
|
"@live-change/cli": "0.7.4",
|
|
25
25
|
"@live-change/dao": "0.5.8",
|
|
26
26
|
"@live-change/dao-vue3": "0.5.8",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"@live-change/framework": "0.7.4",
|
|
29
29
|
"@live-change/image-service": "0.3.2",
|
|
30
30
|
"@live-change/session-service": "0.3.2",
|
|
31
|
-
"@live-change/vue3-components": "0.2.
|
|
32
|
-
"@live-change/vue3-ssr": "0.2.
|
|
31
|
+
"@live-change/vue3-components": "0.2.16",
|
|
32
|
+
"@live-change/vue3-ssr": "0.2.16",
|
|
33
33
|
"@tiptap/extension-highlight": "^2.0.0-beta.33",
|
|
34
34
|
"@tiptap/extension-underline": "2.0.0-beta.23",
|
|
35
35
|
"@tiptap/starter-kit": "^2.0.0-beta.185",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"codeceptjs-assert": "^0.0.5",
|
|
39
39
|
"compression": "^1.7.4",
|
|
40
40
|
"cross-env": "^7.0.3",
|
|
41
|
+
"deepmerge": "^4.2.2",
|
|
41
42
|
"get-port-sync": "1.0.1",
|
|
42
43
|
"pica": "^9.0.1",
|
|
43
44
|
"pretty-bytes": "^6.0.0",
|
|
@@ -66,5 +67,5 @@
|
|
|
66
67
|
"author": "",
|
|
67
68
|
"license": "ISC",
|
|
68
69
|
"description": "",
|
|
69
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "703c9723562ade7b2420e1b25b28ced51a96db09"
|
|
70
71
|
}
|
package/server/init.js
CHANGED
|
@@ -96,6 +96,40 @@ module.exports = async function(services) {
|
|
|
96
96
|
target: 'one'
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
+
await services.content.models.Metadata.create({
|
|
100
|
+
id: App.encodeIdentifier(['content_Page', 'one']),
|
|
101
|
+
objectType: 'content_Page',
|
|
102
|
+
object: 'one',
|
|
103
|
+
title: 'Test Page',
|
|
104
|
+
description: 'Test Description',
|
|
105
|
+
"og": {
|
|
106
|
+
"locale": "en_US",
|
|
107
|
+
"localeAlternate": [],
|
|
108
|
+
"type": "website",
|
|
109
|
+
"music": {
|
|
110
|
+
"song": [],
|
|
111
|
+
"album": [],
|
|
112
|
+
"musician": [],
|
|
113
|
+
"creator": []
|
|
114
|
+
},
|
|
115
|
+
"video": {
|
|
116
|
+
"actor": [],
|
|
117
|
+
"director": [],
|
|
118
|
+
"writer": [],
|
|
119
|
+
"tag": []
|
|
120
|
+
},
|
|
121
|
+
"article": {
|
|
122
|
+
"author": [],
|
|
123
|
+
"tag": []
|
|
124
|
+
},
|
|
125
|
+
"profile": {},
|
|
126
|
+
"book": {
|
|
127
|
+
"author": [],
|
|
128
|
+
"tag": []
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
99
133
|
await createPage('two')
|
|
100
134
|
|
|
101
135
|
await services.url.models.Canonical.create({
|