@crombolo/backoffice-host 0.0.0-stage → 0.10.0
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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/assets/login.js +352 -0
- package/package.json +35 -4
- package/plugin/assistant/client.js +53 -0
- package/plugin/assistant/drawer.js +865 -0
- package/plugin/assistant/umbraco-package.json +16 -0
- package/plugin/branding/backdrop.svg +1 -0
- package/plugin/branding/branding.js +100 -0
- package/plugin/branding/bundle-builder-labels.js +63 -0
- package/plugin/branding/client-credentials.js +49 -0
- package/plugin/branding/components-tree.js +40 -0
- package/plugin/branding/content-type-labels.js +27 -0
- package/plugin/branding/favicon.svg +1 -0
- package/plugin/branding/help-menu.js +46 -0
- package/plugin/branding/icon-mark.js +15 -0
- package/plugin/branding/icon-registry.js +40 -0
- package/plugin/branding/localization-en.js +115 -0
- package/plugin/branding/log-viewer-menu.js +70 -0
- package/plugin/branding/logo-header.svg +1 -0
- package/plugin/branding/logo-surface.svg +1 -0
- package/plugin/branding/query-builder.js +28 -0
- package/plugin/branding/sign-in.js +80 -0
- package/plugin/branding/sysinfo.js +89 -0
- package/plugin/branding/theme.css +102 -0
- package/plugin/bundles-carries.js +16 -0
- package/plugin/bundles-client.js +40 -0
- package/plugin/bundles-created-overview.js +244 -0
- package/plugin/bundles-created.js +70 -0
- package/plugin/bundles-installed.js +176 -0
- package/plugin/bundles-marketplace.js +236 -0
- package/plugin/changes-dashboard.js +169 -0
- package/plugin/element-type-hint.js +58 -0
- package/plugin/form-picker.js +116 -0
- package/plugin/forms-client.js +52 -0
- package/plugin/forms-entries.js +244 -0
- package/plugin/forms-overview.js +380 -0
- package/plugin/localizations/ar.js +97 -0
- package/plugin/localizations/bs.js +86 -0
- package/plugin/localizations/cs.js +73 -0
- package/plugin/localizations/cy.js +92 -0
- package/plugin/localizations/da.js +101 -0
- package/plugin/localizations/de.js +79 -0
- package/plugin/localizations/en.js +71 -0
- package/plugin/localizations/es.js +52 -0
- package/plugin/localizations/fr.js +78 -0
- package/plugin/localizations/he.js +24 -0
- package/plugin/localizations/hr.js +89 -0
- package/plugin/localizations/it.js +86 -0
- package/plugin/localizations/ja.js +43 -0
- package/plugin/localizations/ko.js +24 -0
- package/plugin/localizations/nb.js +44 -0
- package/plugin/localizations/nl.js +85 -0
- package/plugin/localizations/pl.js +44 -0
- package/plugin/localizations/pt-br.js +25 -0
- package/plugin/localizations/pt.js +100 -0
- package/plugin/localizations/ro.js +9 -0
- package/plugin/localizations/ru.js +46 -0
- package/plugin/localizations/sv.js +46 -0
- package/plugin/localizations/tr.js +80 -0
- package/plugin/localizations/uk.js +46 -0
- package/plugin/localizations/umbraco-package.json +279 -0
- package/plugin/localizations/vi.js +104 -0
- package/plugin/localizations/zh-tw.js +38 -0
- package/plugin/localizations/zh.js +38 -0
- package/plugin/read-only-banner.js +48 -0
- package/plugin/report-client.js +13 -0
- package/plugin/seo/client.js +35 -0
- package/plugin/seo/dashboard.js +242 -0
- package/plugin/seo/document-view.js +161 -0
- package/plugin/seo/type-hint.js +45 -0
- package/plugin/seo/umbraco-package.json +61 -0
- package/plugin/settings-dashboard.js +123 -0
- package/plugin/tsx-editors.js +337 -0
- package/plugin/tsx-snippets.js +63 -0
- package/plugin/umbraco-package.json +298 -0
- package/plugin/welcome-dashboard.js +106 -0
- package/src/branding.ts +59 -0
- package/src/bundle-package.ts +266 -0
- package/src/extensions.ts +189 -0
- package/src/graphics.ts +87 -0
- package/src/index.ts +8 -0
- package/src/login-assets.ts +18 -0
- package/src/manifests.ts +154 -0
- package/src/paths.ts +117 -0
- package/src/shell.ts +139 -0
- package/src/static.ts +107 -0
- package/src/text-imports.d.ts +23 -0
- package/README.md +0 -3
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and saves search and sharing at `<backoffice>/crombolo/api/seo/*`. The
|
|
3
|
+
* session cookie carries the auth, as it does for the framework's other plugin
|
|
4
|
+
* endpoints; the base comes from `<base href>` because the backoffice path is
|
|
5
|
+
* configurable.
|
|
6
|
+
*/
|
|
7
|
+
const base = () => {
|
|
8
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
9
|
+
return `${href.replace(/\/$/, '')}/crombolo/api/seo`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const json = async (path, init) => {
|
|
13
|
+
const response = await fetch(`${base()}${path}`, {
|
|
14
|
+
credentials: 'include',
|
|
15
|
+
headers: {
|
|
16
|
+
accept: 'application/json',
|
|
17
|
+
...(init?.body ? { 'content-type': 'application/json' } : {}),
|
|
18
|
+
},
|
|
19
|
+
...init,
|
|
20
|
+
})
|
|
21
|
+
const body = await response.json().catch(() => ({}))
|
|
22
|
+
if (!response.ok) throw new Error(body.message ?? 'Search and sharing could not be loaded.')
|
|
23
|
+
return body
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const readSettings = () => json('/settings')
|
|
27
|
+
|
|
28
|
+
export const saveSettings = (changes) =>
|
|
29
|
+
json('/settings', { method: 'PUT', body: JSON.stringify(changes) })
|
|
30
|
+
|
|
31
|
+
export const readPreview = (documentKey, culture) => {
|
|
32
|
+
const query = new URLSearchParams({ document: documentKey })
|
|
33
|
+
if (culture) query.set('culture', culture)
|
|
34
|
+
return json(`/preview?${query}`)
|
|
35
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search & sharing, in Settings: the site-wide values every page falls back
|
|
3
|
+
* to, the robots file, and the page shown for an address that does not exist.
|
|
4
|
+
*
|
|
5
|
+
* Each field starts from the site's configuration. A value saved here overrides
|
|
6
|
+
* it, and "Use the default" removes the override, so the configuration applies
|
|
7
|
+
* again; the label beside each field says which is in force.
|
|
8
|
+
*/
|
|
9
|
+
// For their side effects: these register `umb-input-document` and
|
|
10
|
+
// `umb-input-media`, which the shell otherwise loads lazily.
|
|
11
|
+
import '@umbraco-cms/backoffice/document'
|
|
12
|
+
import '@umbraco-cms/backoffice/media'
|
|
13
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
14
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
15
|
+
import { readSettings, saveSettings } from './client.js'
|
|
16
|
+
|
|
17
|
+
const SOURCES = {
|
|
18
|
+
backoffice: 'Set here',
|
|
19
|
+
config: 'From the site configuration',
|
|
20
|
+
default: 'Default',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
24
|
+
|
|
25
|
+
export default class CromboloSeoDashboardElement extends UmbLitElement {
|
|
26
|
+
static properties = {
|
|
27
|
+
_settings: { state: true },
|
|
28
|
+
_draft: { state: true },
|
|
29
|
+
_message: { state: true },
|
|
30
|
+
_saving: { state: true },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
super()
|
|
35
|
+
this._draft = {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
connectedCallback() {
|
|
39
|
+
super.connectedCallback()
|
|
40
|
+
this.#load()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #load() {
|
|
44
|
+
try {
|
|
45
|
+
this._settings = await readSettings()
|
|
46
|
+
this._draft = {}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
this._message = error.message
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#value(field) {
|
|
53
|
+
return field in this._draft ? this._draft[field] : this._settings.effective[field]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#set(field, value) {
|
|
57
|
+
this._draft = { ...this._draft, [field]: value }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async #save(changes) {
|
|
61
|
+
this._saving = true
|
|
62
|
+
try {
|
|
63
|
+
this._settings = await saveSettings(changes)
|
|
64
|
+
this._draft = {}
|
|
65
|
+
this._message = 'Saved.'
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this._message = error.message
|
|
68
|
+
} finally {
|
|
69
|
+
this._saving = false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#source(field) {
|
|
74
|
+
const source = this._settings.sources[field]
|
|
75
|
+
return html`
|
|
76
|
+
<span class="source">
|
|
77
|
+
<uui-tag look="secondary">${SOURCES[source]}</uui-tag>
|
|
78
|
+
${
|
|
79
|
+
source === 'backoffice'
|
|
80
|
+
? html`<uui-button
|
|
81
|
+
compact
|
|
82
|
+
look="placeholder"
|
|
83
|
+
label="Use the default"
|
|
84
|
+
@click=${() => this.#save({ [field]: null })}
|
|
85
|
+
></uui-button>`
|
|
86
|
+
: nothing
|
|
87
|
+
}
|
|
88
|
+
</span>
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#field(field, label, description, control) {
|
|
93
|
+
return html`
|
|
94
|
+
<div class="field">
|
|
95
|
+
<div class="label">
|
|
96
|
+
<strong>${label}</strong>
|
|
97
|
+
${this.#source(field)}
|
|
98
|
+
</div>
|
|
99
|
+
<small>${description}</small>
|
|
100
|
+
${control}
|
|
101
|
+
</div>
|
|
102
|
+
`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#toggle(field, label, description) {
|
|
106
|
+
return this.#field(
|
|
107
|
+
field,
|
|
108
|
+
label,
|
|
109
|
+
description,
|
|
110
|
+
html`<uui-toggle
|
|
111
|
+
.checked=${Boolean(this.#value(field))}
|
|
112
|
+
label=${label}
|
|
113
|
+
@change=${(event) => this.#set(field, event.target.checked)}
|
|
114
|
+
></uui-toggle>`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#text(field, label, description, placeholder = '') {
|
|
119
|
+
return this.#field(
|
|
120
|
+
field,
|
|
121
|
+
label,
|
|
122
|
+
description,
|
|
123
|
+
html`<uui-input
|
|
124
|
+
.value=${this.#value(field) ?? ''}
|
|
125
|
+
label=${label}
|
|
126
|
+
placeholder=${placeholder}
|
|
127
|
+
@input=${(event) => this.#set(field, event.target.value)}
|
|
128
|
+
></uui-input>`,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
render() {
|
|
133
|
+
if (!this._settings)
|
|
134
|
+
return this._message
|
|
135
|
+
? html`<uui-box><p>${this._message}</p></uui-box>`
|
|
136
|
+
: html`<uui-loader></uui-loader>`
|
|
137
|
+
const image = this.#value('defaultImage')
|
|
138
|
+
const notFound = this.#value('notFoundPage') ?? []
|
|
139
|
+
const scoped = notFound.some((rule) => rule.host || rule.culture)
|
|
140
|
+
return html`
|
|
141
|
+
<uui-box headline="Search & sharing">
|
|
142
|
+
${this.#toggle(
|
|
143
|
+
'indexable',
|
|
144
|
+
'Let search engines list this site',
|
|
145
|
+
'Turn off for a site that is not live yet, such as a staging copy.',
|
|
146
|
+
)}
|
|
147
|
+
${this.#toggle(
|
|
148
|
+
'sitemap',
|
|
149
|
+
'Offer a list of pages to search engines',
|
|
150
|
+
'Lists every published page at /sitemap.xml, except those hidden from it.',
|
|
151
|
+
)}
|
|
152
|
+
${this.#text('siteName', 'Site name', 'Shown when a page is shared.')}
|
|
153
|
+
${this.#text(
|
|
154
|
+
'titleSuffix',
|
|
155
|
+
'Added to every page title',
|
|
156
|
+
'For example " | Acme". Leave empty for none.',
|
|
157
|
+
)}
|
|
158
|
+
${this.#field(
|
|
159
|
+
'defaultImage',
|
|
160
|
+
'Sharing image',
|
|
161
|
+
'Used when a page without its own image is shared.',
|
|
162
|
+
html`<umb-input-media
|
|
163
|
+
max="1"
|
|
164
|
+
.selection=${UUID.test(image ?? '') ? [image] : []}
|
|
165
|
+
@change=${(event) => this.#set('defaultImage', event.target.selection?.[0] ?? '')}
|
|
166
|
+
></umb-input-media>`,
|
|
167
|
+
)}
|
|
168
|
+
${this.#field(
|
|
169
|
+
'notFoundPage',
|
|
170
|
+
'Page for addresses that do not exist',
|
|
171
|
+
scoped
|
|
172
|
+
? 'The site configuration chooses a page per address or language. Choosing one here uses it everywhere.'
|
|
173
|
+
: 'Shown, with a "not found" status, when nothing else answers an address.',
|
|
174
|
+
html`<umb-input-document
|
|
175
|
+
max="1"
|
|
176
|
+
.selection=${notFound[0]?.document && !scoped ? [notFound[0].document] : []}
|
|
177
|
+
@change=${(event) => {
|
|
178
|
+
const key = event.target.selection?.[0]
|
|
179
|
+
this.#set('notFoundPage', key ? [{ document: key }] : [])
|
|
180
|
+
}}
|
|
181
|
+
></umb-input-document>`,
|
|
182
|
+
)}
|
|
183
|
+
${this.#field(
|
|
184
|
+
'robotsTxt',
|
|
185
|
+
'Instructions for search engines',
|
|
186
|
+
'The rules in /robots.txt. The address of the page list is added for you.',
|
|
187
|
+
html`<uui-textarea
|
|
188
|
+
.value=${this.#value('robotsTxt') ?? ''}
|
|
189
|
+
label="Instructions for search engines"
|
|
190
|
+
@input=${(event) => this.#set('robotsTxt', event.target.value)}
|
|
191
|
+
></uui-textarea>`,
|
|
192
|
+
)}
|
|
193
|
+
<div class="actions">
|
|
194
|
+
<uui-button
|
|
195
|
+
look="primary"
|
|
196
|
+
label="Save"
|
|
197
|
+
?disabled=${this._saving || Object.keys(this._draft).length === 0}
|
|
198
|
+
@click=${() => this.#save(this._draft)}
|
|
199
|
+
></uui-button>
|
|
200
|
+
${this._message ? html`<span>${this._message}</span>` : nothing}
|
|
201
|
+
</div>
|
|
202
|
+
</uui-box>
|
|
203
|
+
`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static styles = css`
|
|
207
|
+
:host {
|
|
208
|
+
display: block;
|
|
209
|
+
padding: var(--uui-size-layout-1);
|
|
210
|
+
}
|
|
211
|
+
.field {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: var(--uui-size-space-2);
|
|
215
|
+
margin-bottom: var(--uui-size-layout-1);
|
|
216
|
+
}
|
|
217
|
+
.label {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: var(--uui-size-space-3);
|
|
221
|
+
}
|
|
222
|
+
.source {
|
|
223
|
+
display: inline-flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: var(--uui-size-space-2);
|
|
226
|
+
}
|
|
227
|
+
small {
|
|
228
|
+
color: var(--uui-color-text-alt);
|
|
229
|
+
}
|
|
230
|
+
uui-textarea {
|
|
231
|
+
--uui-textarea-min-height: 8em;
|
|
232
|
+
font-family: monospace;
|
|
233
|
+
}
|
|
234
|
+
.actions {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: var(--uui-size-space-4);
|
|
238
|
+
}
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
customElements.define('crombolo-seo-dashboard', CromboloSeoDashboardElement)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Search & sharing" view on a page whose type includes those fields: how
|
|
3
|
+
* the page will look in search results, and where each value comes from.
|
|
4
|
+
*
|
|
5
|
+
* The values come from the server, which works them out with the same code the
|
|
6
|
+
* page's head uses, so the preview cannot disagree with what is served. It
|
|
7
|
+
* reads the last saved version, so an editor sees a change once it is saved.
|
|
8
|
+
*/
|
|
9
|
+
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'
|
|
10
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
11
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
12
|
+
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'
|
|
13
|
+
import { readPreview } from './client.js'
|
|
14
|
+
|
|
15
|
+
const SOURCES = {
|
|
16
|
+
page: 'From this page',
|
|
17
|
+
template: 'From the page design',
|
|
18
|
+
site: 'Site setting',
|
|
19
|
+
name: 'Page name',
|
|
20
|
+
none: 'Not set',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default class CromboloSeoDocumentViewElement extends UmbLitElement {
|
|
24
|
+
static properties = {
|
|
25
|
+
_preview: { state: true },
|
|
26
|
+
_error: { state: true },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#unique = undefined
|
|
30
|
+
#culture = null
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super()
|
|
34
|
+
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => {
|
|
35
|
+
if (!context) return
|
|
36
|
+
this.observe(context.unique, (unique) => {
|
|
37
|
+
this.#unique = unique
|
|
38
|
+
this.#load()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (dataset) => {
|
|
42
|
+
this.#culture = dataset?.getVariantId?.()?.culture ?? null
|
|
43
|
+
this.#load()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async #load() {
|
|
48
|
+
if (!this.#unique) return
|
|
49
|
+
try {
|
|
50
|
+
this._preview = await readPreview(this.#unique, this.#culture)
|
|
51
|
+
this._error = undefined
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this._error = error.message
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#row(label, item, render = (value) => value) {
|
|
58
|
+
return html`
|
|
59
|
+
<tr>
|
|
60
|
+
<th>${label}</th>
|
|
61
|
+
<td>${item.value ? render(item.value) : html`<em>Nothing yet</em>`}</td>
|
|
62
|
+
<td><uui-tag look="secondary">${SOURCES[item.source] ?? item.source}</uui-tag></td>
|
|
63
|
+
</tr>
|
|
64
|
+
`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render() {
|
|
68
|
+
if (this._error) return html`<uui-box><p>${this._error}</p></uui-box>`
|
|
69
|
+
const preview = this._preview
|
|
70
|
+
if (!preview) return html`<uui-loader></uui-loader>`
|
|
71
|
+
const { meta, snippet } = preview
|
|
72
|
+
return html`
|
|
73
|
+
<uui-box headline="In search results">
|
|
74
|
+
<div class="snippet">
|
|
75
|
+
<div class="url">${snippet.url || 'This page has no address yet'}</div>
|
|
76
|
+
<div class="title">${snippet.title}</div>
|
|
77
|
+
<div class="description">
|
|
78
|
+
${snippet.description || html`<em>Search engines will pick some text from the page.</em>`}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
${
|
|
82
|
+
meta.noindex.value
|
|
83
|
+
? html`<p class="warning">
|
|
84
|
+
${
|
|
85
|
+
meta.noindex.source === 'site'
|
|
86
|
+
? 'The whole site is hidden from search engines in Settings.'
|
|
87
|
+
: 'This page asks search engines to leave it out of their results.'
|
|
88
|
+
}
|
|
89
|
+
</p>`
|
|
90
|
+
: nothing
|
|
91
|
+
}
|
|
92
|
+
<p class="note">
|
|
93
|
+
Shows the last saved version${preview.published ? '' : ', which is not published yet'}.
|
|
94
|
+
</p>
|
|
95
|
+
</uui-box>
|
|
96
|
+
<uui-box headline="Where each value comes from">
|
|
97
|
+
<table>
|
|
98
|
+
${this.#row('Title', meta.title)}
|
|
99
|
+
${this.#row('Description', meta.description)}
|
|
100
|
+
${this.#row('Sharing image', meta.image, (src) => html`<img src=${src} alt="" />`)}
|
|
101
|
+
${this.#row('Preferred address', meta.canonical)}
|
|
102
|
+
</table>
|
|
103
|
+
</uui-box>
|
|
104
|
+
`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static styles = css`
|
|
108
|
+
:host {
|
|
109
|
+
display: block;
|
|
110
|
+
padding: var(--uui-size-layout-1);
|
|
111
|
+
}
|
|
112
|
+
uui-box + uui-box {
|
|
113
|
+
margin-top: var(--uui-size-layout-1);
|
|
114
|
+
}
|
|
115
|
+
.snippet {
|
|
116
|
+
max-width: 600px;
|
|
117
|
+
font-family: arial, sans-serif;
|
|
118
|
+
}
|
|
119
|
+
.snippet .url {
|
|
120
|
+
color: var(--uui-color-text-alt);
|
|
121
|
+
font-size: 14px;
|
|
122
|
+
overflow-wrap: anywhere;
|
|
123
|
+
}
|
|
124
|
+
.snippet .title {
|
|
125
|
+
color: #1a0dab;
|
|
126
|
+
font-size: 20px;
|
|
127
|
+
margin: 4px 0;
|
|
128
|
+
}
|
|
129
|
+
.snippet .description {
|
|
130
|
+
color: var(--uui-color-text);
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
}
|
|
133
|
+
.warning {
|
|
134
|
+
color: var(--uui-color-danger);
|
|
135
|
+
}
|
|
136
|
+
.note {
|
|
137
|
+
color: var(--uui-color-text-alt);
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
table {
|
|
141
|
+
width: 100%;
|
|
142
|
+
border-collapse: collapse;
|
|
143
|
+
}
|
|
144
|
+
th,
|
|
145
|
+
td {
|
|
146
|
+
text-align: left;
|
|
147
|
+
padding: var(--uui-size-space-3);
|
|
148
|
+
border-bottom: 1px solid var(--uui-color-divider);
|
|
149
|
+
vertical-align: top;
|
|
150
|
+
}
|
|
151
|
+
th {
|
|
152
|
+
width: 12em;
|
|
153
|
+
}
|
|
154
|
+
img {
|
|
155
|
+
max-width: 240px;
|
|
156
|
+
max-height: 140px;
|
|
157
|
+
}
|
|
158
|
+
`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
customElements.define('crombolo-seo-document-view', CromboloSeoDocumentViewElement)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A note in the document-type workspace footer for the "Search & sharing" type:
|
|
3
|
+
* its own fields come with the CMS and stay, but a site may add fields beside
|
|
4
|
+
* them. The server refuses removing or changing them; this says so up front.
|
|
5
|
+
*/
|
|
6
|
+
import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document-type'
|
|
7
|
+
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'
|
|
8
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
9
|
+
|
|
10
|
+
const SEO_FIELDS_KEY = '5e0f1c2a-7b3d-4c8e-9a61-0d2b4f6e8a13'
|
|
11
|
+
|
|
12
|
+
export default class CromboloSeoFieldsHintElement extends UmbElementMixin(LitElement) {
|
|
13
|
+
static properties = { _unique: { state: true } }
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super()
|
|
17
|
+
this._unique = undefined
|
|
18
|
+
this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (context) => {
|
|
19
|
+
if (!context) return
|
|
20
|
+
this.observe(context.unique, (value) => {
|
|
21
|
+
this._unique = value
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render() {
|
|
27
|
+
if (this._unique?.toLowerCase() !== SEO_FIELDS_KEY) return nothing
|
|
28
|
+
return html`<uui-tag
|
|
29
|
+
color="default"
|
|
30
|
+
look="secondary"
|
|
31
|
+
title="Pages that include this type get these fields for search results and sharing. The fields it comes with cannot be removed or changed, but you can add your own beside them."
|
|
32
|
+
>
|
|
33
|
+
Provided by Crombolo — add your own fields beside these
|
|
34
|
+
</uui-tag>`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static styles = css`
|
|
38
|
+
:host {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
customElements.define('crombolo-seo-fields-hint', CromboloSeoFieldsHintElement)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
|
|
3
|
+
"name": "Crombolo Search & sharing",
|
|
4
|
+
"id": "Crombolo.Seo",
|
|
5
|
+
"version": "0.10.0",
|
|
6
|
+
"allowPublicAccess": false,
|
|
7
|
+
"extensions": [
|
|
8
|
+
{
|
|
9
|
+
"type": "dashboard",
|
|
10
|
+
"alias": "Crombolo.Dashboard.Seo",
|
|
11
|
+
"name": "Crombolo Search & Sharing Dashboard",
|
|
12
|
+
"element": "/crombolo-plugin/seo/dashboard.js",
|
|
13
|
+
"weight": 10,
|
|
14
|
+
"meta": {
|
|
15
|
+
"label": "Search & sharing",
|
|
16
|
+
"pathname": "search-and-sharing"
|
|
17
|
+
},
|
|
18
|
+
"conditions": [
|
|
19
|
+
{
|
|
20
|
+
"alias": "Umb.Condition.SectionAlias",
|
|
21
|
+
"match": "Umb.Section.Settings"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "workspaceView",
|
|
27
|
+
"alias": "Crombolo.WorkspaceView.Document.Seo",
|
|
28
|
+
"name": "Crombolo Search & Sharing Preview",
|
|
29
|
+
"element": "/crombolo-plugin/seo/document-view.js",
|
|
30
|
+
"weight": 90,
|
|
31
|
+
"meta": {
|
|
32
|
+
"label": "Search & sharing",
|
|
33
|
+
"pathname": "search-and-sharing",
|
|
34
|
+
"icon": "icon-search"
|
|
35
|
+
},
|
|
36
|
+
"conditions": [
|
|
37
|
+
{
|
|
38
|
+
"alias": "Umb.Condition.WorkspaceAlias",
|
|
39
|
+
"match": "Umb.Workspace.Document"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"alias": "Umb.Condition.WorkspaceContentTypeAlias",
|
|
43
|
+
"match": "seoFields"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "workspaceFooterApp",
|
|
49
|
+
"alias": "Crombolo.WorkspaceFooterApp.SeoFieldsHint",
|
|
50
|
+
"name": "Crombolo Search & Sharing Type Hint",
|
|
51
|
+
"element": "/crombolo-plugin/seo/type-hint.js",
|
|
52
|
+
"weight": 900,
|
|
53
|
+
"conditions": [
|
|
54
|
+
{
|
|
55
|
+
"alias": "Umb.Condition.WorkspaceAlias",
|
|
56
|
+
"match": "Umb.Workspace.DocumentType"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Settings dashboard, in place of Umbraco's.
|
|
3
|
+
*
|
|
4
|
+
* Its manifest declares `overwrites: ['Umb.Dashboard.SettingsWelcome']`, the same
|
|
5
|
+
* way the Welcome dashboard takes the place of the news one. What it replaces is
|
|
6
|
+
* marketing for Umbraco's own training and consultancy — accurate for Umbraco and
|
|
7
|
+
* misleading here, and not the sort of thing to fix by renaming, since the
|
|
8
|
+
* services it points at are real and are not this project's.
|
|
9
|
+
*
|
|
10
|
+
* What it says instead is the thing a person in Settings actually needs to know:
|
|
11
|
+
* that types are files, and that changing one here writes that file.
|
|
12
|
+
*/
|
|
13
|
+
// UmbLitElement, not plain LitElement: a dashboard is used as a controller host,
|
|
14
|
+
// and a plain element fails with `controllerHost.provideContext is not a function`.
|
|
15
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
16
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
17
|
+
|
|
18
|
+
/** Whether this site can send e-mail; the server answers with the reason. */
|
|
19
|
+
const emailStatus = async () => {
|
|
20
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
21
|
+
const response = await fetch(`${href.replace(/\/$/, '')}/crombolo/api/email`, {
|
|
22
|
+
credentials: 'include',
|
|
23
|
+
headers: { accept: 'application/json' },
|
|
24
|
+
})
|
|
25
|
+
if (!response.ok) throw new Error(`email responded ${response.status}`)
|
|
26
|
+
return response.json()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default class CromboloSettingsDashboardElement extends UmbLitElement {
|
|
30
|
+
static properties = {
|
|
31
|
+
_email: { state: true },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
connectedCallback() {
|
|
35
|
+
super.connectedCallback()
|
|
36
|
+
// A failure here leaves the section out rather than the dashboard broken:
|
|
37
|
+
// this is an explanation, not a control.
|
|
38
|
+
emailStatus().then(
|
|
39
|
+
(status) => {
|
|
40
|
+
this._email = status
|
|
41
|
+
},
|
|
42
|
+
() => {},
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#email() {
|
|
47
|
+
const status = this._email
|
|
48
|
+
if (!status) return nothing
|
|
49
|
+
if (status.available)
|
|
50
|
+
return html`
|
|
51
|
+
<uui-box headline="E-mail">
|
|
52
|
+
<p>E-mail is sent through <strong>${status.provider}</strong>.</p>
|
|
53
|
+
</uui-box>
|
|
54
|
+
`
|
|
55
|
+
return html`
|
|
56
|
+
<uui-box headline="E-mail">
|
|
57
|
+
<p>${status.reason}</p>
|
|
58
|
+
${
|
|
59
|
+
status.affects.length > 0
|
|
60
|
+
? html`<p>Until one is configured, these are unavailable:</p>
|
|
61
|
+
<ul>
|
|
62
|
+
${status.affects.map((feature) => html`<li>${feature}</li>`)}
|
|
63
|
+
</ul>`
|
|
64
|
+
: nothing
|
|
65
|
+
}
|
|
66
|
+
</uui-box>
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
render() {
|
|
71
|
+
return html`
|
|
72
|
+
<uui-box headline="Settings">
|
|
73
|
+
<p>
|
|
74
|
+
Content types, data types and languages are <strong>files</strong>, in
|
|
75
|
+
<code>schema/</code>, written as TOML. The database is a view of them: what you change
|
|
76
|
+
here is written to the file, and the file is what a deploy and every other node reads.
|
|
77
|
+
</p>
|
|
78
|
+
<p>
|
|
79
|
+
That is why a type can be reviewed in a pull request, and why a schema change carries a
|
|
80
|
+
version — a change that converts existing content is a major, and the backoffice moves
|
|
81
|
+
the number for you.
|
|
82
|
+
</p>
|
|
83
|
+
<p class="links">
|
|
84
|
+
<a href="https://github.com/kineco-au/crombolo-cms#schema-as-code" target="_blank" rel="noopener">
|
|
85
|
+
Schema as code
|
|
86
|
+
</a>
|
|
87
|
+
<a href="https://github.com/kineco-au/crombolo-cms#schema-in-shared-storage" target="_blank" rel="noopener">
|
|
88
|
+
Schema in shared storage
|
|
89
|
+
</a>
|
|
90
|
+
<a href="https://github.com/kineco-au/crombolo-cms/issues" target="_blank" rel="noopener">
|
|
91
|
+
Report an issue
|
|
92
|
+
</a>
|
|
93
|
+
</p>
|
|
94
|
+
</uui-box>
|
|
95
|
+
${this.#email()}
|
|
96
|
+
`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static styles = css`
|
|
100
|
+
:host {
|
|
101
|
+
display: block;
|
|
102
|
+
padding: var(--uui-size-layout-1, 24px);
|
|
103
|
+
}
|
|
104
|
+
p {
|
|
105
|
+
max-width: 60ch;
|
|
106
|
+
line-height: 1.5;
|
|
107
|
+
}
|
|
108
|
+
.links {
|
|
109
|
+
display: flex;
|
|
110
|
+
gap: var(--uui-size-space-5, 18px);
|
|
111
|
+
flex-wrap: wrap;
|
|
112
|
+
}
|
|
113
|
+
uui-box + uui-box {
|
|
114
|
+
margin-top: var(--uui-size-layout-1, 24px);
|
|
115
|
+
}
|
|
116
|
+
ul {
|
|
117
|
+
max-width: 60ch;
|
|
118
|
+
line-height: 1.5;
|
|
119
|
+
}
|
|
120
|
+
`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
customElements.define('crombolo-settings-dashboard', CromboloSettingsDashboardElement)
|