@edgedev/create-edge-app 1.1.28 → 1.1.29

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.
@@ -77,6 +77,47 @@ const register = reactive({
77
77
  requestedOrgId: '',
78
78
  })
79
79
 
80
+ const resolveAuthEmail = () => {
81
+ return (
82
+ edgeFirebase?.user?.email
83
+ || edgeFirebase?.user?.firebaseUser?.email
84
+ || edgeFirebase?.user?.firebaseUser?.providerData?.find(p => p?.email)?.email
85
+ || ''
86
+ )
87
+ }
88
+
89
+ const waitForUserSnapshot = async (timeoutMs = 8000) => {
90
+ const findUser = () => {
91
+ const users = Object.values(edgeFirebase.state?.users || {})
92
+ const stagedDocId = edgeFirebase?.user?.stagedDocId
93
+ const uid = edgeFirebase?.user?.uid
94
+ return users.find(u => (stagedDocId && u?.docId === stagedDocId) || (uid && u?.userId === uid))
95
+ }
96
+
97
+ if (findUser())
98
+ return true
99
+
100
+ return await new Promise((resolve) => {
101
+ let timeoutId = null
102
+ const stop = watch(
103
+ () => edgeFirebase.state?.users,
104
+ () => {
105
+ if (findUser()) {
106
+ stop()
107
+ if (timeoutId)
108
+ clearTimeout(timeoutId)
109
+ resolve(true)
110
+ }
111
+ },
112
+ { immediate: true, deep: true },
113
+ )
114
+ timeoutId = setTimeout(() => {
115
+ stop()
116
+ resolve(false)
117
+ }, timeoutMs)
118
+ })
119
+ }
120
+
80
121
  const onSubmit = async () => {
81
122
  state.registering = true
82
123
 
@@ -108,6 +149,9 @@ const onSubmit = async () => {
108
149
  if (state.showRegistrationCode || !props.registrationCode) {
109
150
  register.registrationCode = state.registrationCode
110
151
  }
152
+ if (state.provider === 'email' && register.email) {
153
+ register.meta.email = register.email
154
+ }
111
155
  const result = await edgeFirebase.registerUser(register, state.provider)
112
156
  state.error.error = !result.success
113
157
  if (result.message === `${props.requestedOrgIdLabel} already exists.`) {
@@ -118,6 +162,13 @@ const onSubmit = async () => {
118
162
  result.message = `${orgLabel} already exists. Please choose another.`
119
163
  }
120
164
  state.error.message = result.message.code ? result.message.code : result.message
165
+ if (result.success) {
166
+ const authEmail = resolveAuthEmail()
167
+ if (authEmail && (!register.meta.email || register.meta.email !== authEmail)) {
168
+ await waitForUserSnapshot()
169
+ await edgeFirebase.setUserMeta({ email: authEmail })
170
+ }
171
+ }
121
172
  }
122
173
 
123
174
  state.registering = false
@@ -122,11 +122,13 @@ const sanitizeQueryItems = (meta) => {
122
122
  return cleaned
123
123
  }
124
124
 
125
- const resetArrayItems = (field) => {
125
+ const resetArrayItems = (field, metaSource = null) => {
126
+ const meta = metaSource || modelValue.value?.meta || {}
127
+ const fieldMeta = meta?.[field]
126
128
  if (!state.arrayItems?.[field]) {
127
129
  state.arrayItems[field] = {}
128
130
  }
129
- for (const schemaItem of modelValue.value.meta[field].schema) {
131
+ for (const schemaItem of (fieldMeta?.schema || [])) {
130
132
  if (schemaItem.type === 'text') {
131
133
  state.arrayItems[field][schemaItem.field] = ''
132
134
  }
@@ -148,25 +150,35 @@ const resetArrayItems = (field) => {
148
150
  const openEditor = async () => {
149
151
  if (!props.editMode)
150
152
  return
151
- for (const key of Object.keys(modelValue.value?.meta || {})) {
152
- if (modelValue.value.meta[key]?.type === 'array' && modelValue.value.meta[key]?.schema) {
153
- if (!modelValue.value.meta[key]?.api) {
154
- resetArrayItems(key)
155
- }
153
+ const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
154
+ const templateMeta = blockData?.meta || modelValue.value?.meta || {}
155
+ const storedMeta = modelValue.value?.meta || {}
156
+ const mergedMeta = edgeGlobal.dupObject(templateMeta) || {}
157
+
158
+ for (const key of Object.keys(mergedMeta)) {
159
+ const storedField = storedMeta?.[key]
160
+ if (!storedField || typeof storedField !== 'object')
161
+ continue
162
+ if (storedField.queryItems && typeof storedField.queryItems === 'object') {
163
+ mergedMeta[key].queryItems = edgeGlobal.dupObject(storedField.queryItems)
164
+ }
165
+ if (storedField.limit !== undefined) {
166
+ mergedMeta[key].limit = storedField.limit
156
167
  }
157
168
  }
158
- state.draft = JSON.parse(JSON.stringify(modelValue.value?.values || {}))
159
- state.meta = JSON.parse(JSON.stringify(modelValue.value?.meta || {}))
160
- ensureQueryItemsDefaults(state.meta)
161
- const blockData = edgeFirebase.data[`${edgeGlobal.edgeState.organizationDocPath}/blocks`]?.[modelValue.value.blockId]
162
- state.metaUpdate = edgeGlobal.dupObject(modelValue.value?.meta) || {}
163
- if (blockData?.meta) {
164
- for (const key of Object.keys(blockData.meta)) {
165
- if (!(key in state.metaUpdate)) {
166
- state.metaUpdate[key] = blockData.meta[key]
169
+
170
+ for (const key of Object.keys(mergedMeta || {})) {
171
+ if (mergedMeta[key]?.type === 'array' && mergedMeta[key]?.schema) {
172
+ if (!mergedMeta[key]?.api) {
173
+ resetArrayItems(key, mergedMeta)
167
174
  }
168
175
  }
169
176
  }
177
+
178
+ state.draft = JSON.parse(JSON.stringify(modelValue.value?.values || {}))
179
+ state.meta = JSON.parse(JSON.stringify(mergedMeta || {}))
180
+ ensureQueryItemsDefaults(state.meta)
181
+ state.metaUpdate = edgeGlobal.dupObject(mergedMeta) || {}
170
182
  if (blockData?.values) {
171
183
  for (const key of Object.keys(blockData.values)) {
172
184
  if (!(key in state.draft)) {
@@ -682,6 +694,7 @@ const getTagsFromPosts = computed(() => {
682
694
  v-model="state.meta[entry.field].queryItems[option.field]"
683
695
  :option="option"
684
696
  :label="genTitleFromField(option)"
697
+ :multiple="option?.multiple || false"
685
698
  />
686
699
  </div>
687
700
  </template>
@@ -56,12 +56,34 @@ const props = defineProps({
56
56
  type: String,
57
57
  default: '',
58
58
  },
59
+ validateJson: {
60
+ type: Boolean,
61
+ default: false,
62
+ },
59
63
  })
60
64
 
61
65
  const emit = defineEmits(['update:modelValue', 'lineClick'])
62
66
  const localModelValue = ref(null)
63
67
  const edgeFirebase = inject('edgeFirebase')
64
68
  const expectsJsonObject = ref(false)
69
+ const jsonValidationError = computed(() => {
70
+ if (!props.validateJson || props.language !== 'json')
71
+ return ''
72
+ const raw = localModelValue.value
73
+ if (raw === null || raw === undefined)
74
+ return ''
75
+ const text = String(raw).trim()
76
+ if (!text)
77
+ return ''
78
+ try {
79
+ JSON.parse(text)
80
+ return ''
81
+ }
82
+ catch (error) {
83
+ return `Invalid JSON: ${error.message}`
84
+ }
85
+ })
86
+ const displayError = computed(() => props.error || jsonValidationError.value)
65
87
 
66
88
  const editorOptions = {
67
89
  mode: 'htmlmixed',
@@ -379,11 +401,11 @@ onBeforeUnmount(() => {
379
401
  </template>
380
402
  <template #center>
381
403
  <div class="w-full px-2">
382
- <Alert v-if="props.error" variant="destructive" class="rounded-[6px] py-1">
404
+ <Alert v-if="displayError" variant="destructive" class="rounded-[6px] py-1">
383
405
  <TriangleAlert class="h-4 w-4" />
384
406
  <AlertTitle>Error</AlertTitle>
385
407
  <AlertDescription>
386
- {{ props.error }}
408
+ {{ displayError }}
387
409
  </AlertDescription>
388
410
  </Alert>
389
411
  </div>
@@ -699,10 +699,18 @@ function rewriteAllClasses(scopeEl, theme, isolated = true, viewportMode = 'auto
699
699
  scopeEl.querySelectorAll('[class]').forEach((el) => {
700
700
  let base = el.dataset.viewportBaseClass
701
701
  if (typeof base !== 'string') {
702
- base = el.className || ''
702
+ if (typeof el.className === 'string') {
703
+ base = el.className
704
+ }
705
+ else if (el.className && typeof el.className.baseVal === 'string') {
706
+ base = el.className.baseVal
707
+ }
708
+ else {
709
+ base = el.getAttribute('class') || ''
710
+ }
703
711
  el.dataset.viewportBaseClass = base
704
712
  }
705
- const orig = base || ''
713
+ const orig = typeof base === 'string' ? base : String(base || '')
706
714
  if (!orig.trim())
707
715
  return
708
716
  const origTokens = orig.split(/\s+/).filter(Boolean)
@@ -8,6 +8,11 @@ const props = defineProps({
8
8
  required: false,
9
9
  default: 'all',
10
10
  },
11
+ includeCmsAll: {
12
+ type: Boolean,
13
+ required: false,
14
+ default: true,
15
+ },
11
16
  selectMode: {
12
17
  type: Boolean,
13
18
  required: false,
@@ -137,6 +142,7 @@ onBeforeMount(() => {
137
142
  console.log('Default tags prop:', props.defaultTags)
138
143
  if (props.defaultTags && Array.isArray(props.defaultTags) && props.defaultTags.length > 0) {
139
144
  state.filterTags = [...props.defaultTags]
145
+ state.tags = [...props.defaultTags]
140
146
  }
141
147
  })
142
148
 
@@ -166,6 +172,12 @@ const isLightName = (name) => {
166
172
  }
167
173
 
168
174
  const previewBackgroundClass = computed(() => (isLightName(state.workingDoc?.name) ? 'bg-neutral-900/90' : 'bg-neutral-100'))
175
+
176
+ const siteQueryValue = computed(() => {
177
+ if (!props.site)
178
+ return []
179
+ return props.includeCmsAll ? ['all', props.site] : [props.site]
180
+ })
169
181
  </script>
170
182
 
171
183
  <template>
@@ -228,8 +240,12 @@ const previewBackgroundClass = computed(() => (isLightName(state.workingDoc?.nam
228
240
  v-if="state.tags.length === 0"
229
241
  class="pointer-events-auto absolute inset-0 z-20 rounded-[20px] border border-dashed border-border/70 bg-background/85 dark:bg-background/80 backdrop-blur-sm flex flex-col items-center justify-center text-center px-6 text-foreground"
230
242
  >
231
- <div class="text-lg font-semibold">Tags are required</div>
232
- <div class="text-sm text-muted-foreground">Add tags above to enable upload</div>
243
+ <div class="text-lg font-semibold">
244
+ Tags are required
245
+ </div>
246
+ <div class="text-sm text-muted-foreground">
247
+ Add tags above to enable upload
248
+ </div>
233
249
  </div>
234
250
  </div>
235
251
  </SheetContent>
@@ -239,7 +255,7 @@ const previewBackgroundClass = computed(() => (isLightName(state.workingDoc?.nam
239
255
  sort-field="uploadTime"
240
256
  query-field="meta.cmssite"
241
257
  :filters="filters"
242
- :query-value="['all', props.site]"
258
+ :query-value="siteQueryValue"
243
259
  query-operator="array-contains-any"
244
260
  header-class=""
245
261
  sort-direction="desc" class="w-full flex-1 border-none shadow-none bg-background"
@@ -75,6 +75,42 @@ const normalizeForCompare = (value) => {
75
75
 
76
76
  const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
77
77
  const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
78
+ const isBlankString = value => String(value || '').trim() === ''
79
+ const isJsonInvalid = (value) => {
80
+ if (value === null || value === undefined)
81
+ return false
82
+ if (typeof value === 'object')
83
+ return false
84
+ const text = String(value).trim()
85
+ if (!text)
86
+ return false
87
+ try {
88
+ JSON.parse(text)
89
+ return false
90
+ }
91
+ catch {
92
+ return true
93
+ }
94
+ }
95
+ const hasStructuredDataErrors = (doc) => {
96
+ if (!doc)
97
+ return false
98
+ if (isJsonInvalid(doc.structuredData))
99
+ return true
100
+ if (doc.post && isJsonInvalid(doc.postStructuredData))
101
+ return true
102
+ return false
103
+ }
104
+ const ensurePostSeoDefaults = (doc) => {
105
+ if (!doc?.post)
106
+ return
107
+ if (isBlankString(doc.postMetaTitle))
108
+ doc.postMetaTitle = doc.metaTitle || ''
109
+ if (isBlankString(doc.postMetaDescription))
110
+ doc.postMetaDescription = doc.metaDescription || ''
111
+ if (isBlankString(doc.postStructuredData))
112
+ doc.postStructuredData = doc.structuredData || buildPageStructuredData()
113
+ }
78
114
 
79
115
  const orderedMenus = computed(() => {
80
116
  const menuEntries = Object.entries(modelValue.value || {}).map(([name, menu], originalIndex) => ({
@@ -117,6 +153,9 @@ const isPublishedPageDiff = (pageId) => {
117
153
  metaTitle: publishedPage.metaTitle,
118
154
  metaDescription: publishedPage.metaDescription,
119
155
  structuredData: publishedPage.structuredData,
156
+ postMetaTitle: publishedPage.postMetaTitle,
157
+ postMetaDescription: publishedPage.postMetaDescription,
158
+ postStructuredData: publishedPage.postStructuredData,
120
159
  },
121
160
  {
122
161
  content: draftPage.content,
@@ -126,6 +165,9 @@ const isPublishedPageDiff = (pageId) => {
126
165
  metaTitle: draftPage.metaTitle,
127
166
  metaDescription: draftPage.metaDescription,
128
167
  structuredData: draftPage.structuredData,
168
+ postMetaTitle: draftPage.postMetaTitle,
169
+ postMetaDescription: draftPage.postMetaDescription,
170
+ postStructuredData: draftPage.postStructuredData,
129
171
  },
130
172
  )
131
173
  }
@@ -172,6 +214,9 @@ const state = reactive({
172
214
  metaTitle: { value: '' },
173
215
  metaDescription: { value: '' },
174
216
  structuredData: { value: buildPageStructuredData() },
217
+ postMetaTitle: { value: '' },
218
+ postMetaDescription: { value: '' },
219
+ postStructuredData: { value: '' },
175
220
  tags: { value: [] },
176
221
  allowedThemes: { value: [] },
177
222
  },
@@ -526,6 +571,9 @@ const buildPagePayloadFromTemplate = (templateDoc, slug) => {
526
571
  metaTitle: '',
527
572
  metaDescription: '',
528
573
  structuredData,
574
+ postMetaTitle: '',
575
+ postMetaDescription: '',
576
+ postStructuredData: '',
529
577
  doc_created_at: timestamp,
530
578
  last_updated: timestamp,
531
579
  }
@@ -797,6 +845,10 @@ const showPageSettings = (page) => {
797
845
  state.pageSettings = true
798
846
  }
799
847
 
848
+ const handlePageWorkingDoc = (doc) => {
849
+ ensurePostSeoDefaults(doc)
850
+ }
851
+
800
852
  const formErrors = (error) => {
801
853
  console.log('Form errors:', error)
802
854
  console.log(Object.values(error))
@@ -1214,6 +1266,7 @@ const theme = computed(() => {
1214
1266
  :save-function-override="onSubmit"
1215
1267
  card-content-class="px-0"
1216
1268
  @error="formErrors"
1269
+ @working-doc="handlePageWorkingDoc"
1217
1270
  >
1218
1271
  <template #main="slotProps">
1219
1272
  <div class="p-6 space-y-4 h-[calc(100vh-142px)] overflow-y-auto">
@@ -1253,24 +1306,83 @@ const theme = computed(() => {
1253
1306
  <CardDescription>Meta tags for the page.</CardDescription>
1254
1307
  </CardHeader>
1255
1308
  <CardContent class="pt-0">
1256
- <edge-shad-input
1257
- v-model="slotProps.workingDoc.metaTitle"
1258
- label="Meta Title"
1259
- name="metaTitle"
1260
- />
1261
- <edge-shad-textarea
1262
- v-model="slotProps.workingDoc.metaDescription"
1263
- label="Meta Description"
1264
- name="metaDescription"
1265
- />
1266
- <edge-cms-code-editor
1267
- v-model="slotProps.workingDoc.structuredData"
1268
- title="Structured Data (JSON-LD)"
1269
- language="json"
1270
- name="structuredData"
1271
- height="300px"
1272
- class="mb-4 w-full"
1273
- />
1309
+ <Tabs class="w-full" default-value="list">
1310
+ <TabsList v-if="slotProps.workingDoc?.post" class="w-full grid grid-cols-2 gap-1 rounded-lg border border-border/60 bg-muted/30 p-1">
1311
+ <TabsTrigger
1312
+ value="list"
1313
+ class="text-xs font-semibold uppercase tracking-wide transition-all data-[state=active]:bg-slate-900 data-[state=active]:text-white data-[state=active]:shadow-sm"
1314
+ >
1315
+ Index Page
1316
+ </TabsTrigger>
1317
+ <TabsTrigger
1318
+ value="post"
1319
+ class="text-xs font-semibold uppercase tracking-wide transition-all data-[state=active]:bg-slate-900 data-[state=active]:text-white data-[state=active]:shadow-sm"
1320
+ >
1321
+ Detail Page
1322
+ </TabsTrigger>
1323
+ </TabsList>
1324
+ <TabsContent value="list" class="mt-4 space-y-4">
1325
+ <edge-shad-input
1326
+ v-model="slotProps.workingDoc.metaTitle"
1327
+ label="Meta Title"
1328
+ name="metaTitle"
1329
+ />
1330
+ <edge-shad-textarea
1331
+ v-model="slotProps.workingDoc.metaDescription"
1332
+ label="Meta Description"
1333
+ name="metaDescription"
1334
+ />
1335
+ <div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
1336
+ CMS tokens in double curly braces are replaced on the front end.
1337
+ Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
1338
+ <span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
1339
+ <span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
1340
+ </div>
1341
+ <edge-cms-code-editor
1342
+ v-model="slotProps.workingDoc.structuredData"
1343
+ title="Structured Data (JSON-LD)"
1344
+ language="json"
1345
+ name="structuredData"
1346
+ validate-json
1347
+ height="300px"
1348
+ class="mb-4 w-full"
1349
+ />
1350
+ </TabsContent>
1351
+ <TabsContent value="post" class="mt-4 space-y-4">
1352
+ <div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
1353
+ You can use template keys in double curly braces to pull data from the detail record.
1354
+ Example: <span v-pre class="font-semibold text-foreground">"{{name}}"</span> will be replaced with the record’s name.
1355
+ Dot notation is supported for nested objects, e.g. <span v-pre class="font-semibold text-foreground">"{{data.name}}"</span>.
1356
+ These keys work in the Title, Description, and Structured Data fields.
1357
+ </div>
1358
+ <edge-shad-input
1359
+ v-model="slotProps.workingDoc.postMetaTitle"
1360
+ label="Meta Title"
1361
+ name="postMetaTitle"
1362
+ />
1363
+ <edge-shad-textarea
1364
+ v-model="slotProps.workingDoc.postMetaDescription"
1365
+ label="Meta Description"
1366
+ name="postMetaDescription"
1367
+ />
1368
+
1369
+ <div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
1370
+ CMS tokens in double curly braces are replaced on the front end.
1371
+ Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
1372
+ <span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
1373
+ <span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
1374
+ </div>
1375
+ <edge-cms-code-editor
1376
+ v-model="slotProps.workingDoc.postStructuredData"
1377
+ title="Structured Data (JSON-LD)"
1378
+ language="json"
1379
+ name="postStructuredData"
1380
+ validate-json
1381
+ height="300px"
1382
+ class="mb-4 w-full"
1383
+ />
1384
+ </TabsContent>
1385
+ </Tabs>
1274
1386
  </CardContent>
1275
1387
  </Card>
1276
1388
  </div>
@@ -1278,7 +1390,7 @@ const theme = computed(() => {
1278
1390
  <edge-shad-button variant="destructive" class="text-white" @click="state.pageSettings = false">
1279
1391
  Cancel
1280
1392
  </edge-shad-button>
1281
- <edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1393
+ <edge-shad-button :disabled="slotProps.submitting || hasStructuredDataErrors(slotProps.workingDoc)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1282
1394
  <Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
1283
1395
  Update
1284
1396
  </edge-shad-button>
@@ -2,7 +2,7 @@
2
2
  import { useVModel } from '@vueuse/core'
3
3
  const props = defineProps({
4
4
  modelValue: {
5
- type: [String, Boolean, Number, null],
5
+ type: [String, Boolean, Number, Array, null],
6
6
  required: false,
7
7
  default: null,
8
8
  },
@@ -14,6 +14,10 @@ const props = defineProps({
14
14
  type: String,
15
15
  required: false,
16
16
  },
17
+ multiple: {
18
+ type: Boolean,
19
+ default: false,
20
+ },
17
21
  })
18
22
  const emits = defineEmits(['update:modelValue'])
19
23
  const edgeFirebase = inject('edgeFirebase')
@@ -91,17 +95,30 @@ onBeforeMount(async () => {
91
95
  })
92
96
  .filter(Boolean) // remove nulls
93
97
  }
94
- staticOption.options.unshift({ title: '(none)', name: NONE_VALUE })
98
+ if (!props.multiple) {
99
+ staticOption.options.unshift({ title: '(none)', name: NONE_VALUE })
100
+ }
95
101
  state.loading = false
96
102
  })
97
103
  </script>
98
104
 
99
105
  <template>
100
106
  <edge-shad-select
101
- v-if="!state.loading && staticOption.options.length > 0"
107
+ v-if="!state.loading && staticOption.options.length > 0 && !props.multiple"
102
108
  v-model="selectValue"
103
109
  :label="props.label"
104
110
  :name="props.option.field"
105
111
  :items="staticOption.options"
106
112
  />
113
+ <edge-shad-select-tags
114
+ v-else-if="!state.loading && staticOption.options.length > 0 && props.multiple"
115
+ :model-value="Array.isArray(modelValue) ? modelValue : []"
116
+ :label="props.label"
117
+ :name="props.option.field"
118
+ :items="staticOption.options"
119
+ item-title="title"
120
+ item-value="name"
121
+ :allow-additions="false"
122
+ @update:model-value="value => (modelValue = Array.isArray(value) ? value : [])"
123
+ />
107
124
  </template>
@@ -881,6 +881,9 @@ const isPublishedPageDiff = (pageId) => {
881
881
  metaTitle: publishedPage.metaTitle,
882
882
  metaDescription: publishedPage.metaDescription,
883
883
  structuredData: publishedPage.structuredData,
884
+ postMetaTitle: publishedPage.postMetaTitle,
885
+ postMetaDescription: publishedPage.postMetaDescription,
886
+ postStructuredData: publishedPage.postStructuredData,
884
887
  },
885
888
  {
886
889
  content: draftPage.content,
@@ -890,6 +893,9 @@ const isPublishedPageDiff = (pageId) => {
890
893
  metaTitle: draftPage.metaTitle,
891
894
  metaDescription: draftPage.metaDescription,
892
895
  structuredData: draftPage.structuredData,
896
+ postMetaTitle: draftPage.postMetaTitle,
897
+ postMetaDescription: draftPage.postMetaDescription,
898
+ postStructuredData: draftPage.postStructuredData,
893
899
  },
894
900
  )
895
901
  }
@@ -1087,6 +1093,9 @@ const unpublishedChangeDetails = computed(() => {
1087
1093
  compareField('metaTitle', 'Meta title', val => summarizeChangeValue(val, true))
1088
1094
  compareField('metaDescription', 'Meta description', val => summarizeChangeValue(val, true))
1089
1095
  compareField('structuredData', 'Structured data', val => summarizeChangeValue(val, true))
1096
+ compareField('postMetaTitle', 'Detail meta title', val => summarizeChangeValue(val, true))
1097
+ compareField('postMetaDescription', 'Detail meta description', val => summarizeChangeValue(val, true))
1098
+ compareField('postStructuredData', 'Detail structured data', val => summarizeChangeValue(val, true))
1090
1099
 
1091
1100
  return changes
1092
1101
  })
@@ -33,6 +33,22 @@ const normalizeForCompare = (value) => {
33
33
 
34
34
  const stableSerialize = value => JSON.stringify(normalizeForCompare(value))
35
35
  const areEqualNormalized = (a, b) => stableSerialize(a) === stableSerialize(b)
36
+ const isJsonInvalid = (value) => {
37
+ if (value === null || value === undefined)
38
+ return false
39
+ if (typeof value === 'object')
40
+ return false
41
+ const text = String(value).trim()
42
+ if (!text)
43
+ return false
44
+ try {
45
+ JSON.parse(text)
46
+ return false
47
+ }
48
+ catch {
49
+ return true
50
+ }
51
+ }
36
52
 
37
53
  const isTemplateSite = computed(() => props.site === 'templates')
38
54
  const router = useRouter()
@@ -339,8 +355,9 @@ const themeOptionsMap = computed(() => {
339
355
  const orgUsers = computed(() => edgeFirebase.state?.users || {})
340
356
  const userOptions = computed(() => {
341
357
  return Object.entries(orgUsers.value || {})
358
+ .filter(([, user]) => Boolean(user?.userId))
342
359
  .map(([id, user]) => ({
343
- value: user?.userId || id,
360
+ value: user?.userId,
344
361
  label: user?.meta?.name || user?.userId || id,
345
362
  }))
346
363
  .sort((a, b) => a.label.localeCompare(b.label))
@@ -859,6 +876,9 @@ const isPublishedPageDiff = (pageId) => {
859
876
  metaTitle: publishedPage.metaTitle,
860
877
  metaDescription: publishedPage.metaDescription,
861
878
  structuredData: publishedPage.structuredData,
879
+ postMetaTitle: publishedPage.postMetaTitle,
880
+ postMetaDescription: publishedPage.postMetaDescription,
881
+ postStructuredData: publishedPage.postStructuredData,
862
882
  },
863
883
  {
864
884
  content: draftPage.content,
@@ -868,6 +888,9 @@ const isPublishedPageDiff = (pageId) => {
868
888
  metaTitle: draftPage.metaTitle,
869
889
  metaDescription: draftPage.metaDescription,
870
890
  structuredData: draftPage.structuredData,
891
+ postMetaTitle: draftPage.postMetaTitle,
892
+ postMetaDescription: draftPage.postMetaDescription,
893
+ postStructuredData: draftPage.postStructuredData,
871
894
  },
872
895
  )
873
896
  }
@@ -1069,6 +1092,9 @@ const isAnyPagesDiff = computed(() => {
1069
1092
  metaTitle: pageData.metaTitle,
1070
1093
  metaDescription: pageData.metaDescription,
1071
1094
  structuredData: pageData.structuredData,
1095
+ postMetaTitle: pageData.postMetaTitle,
1096
+ postMetaDescription: pageData.postMetaDescription,
1097
+ postStructuredData: pageData.postStructuredData,
1072
1098
  },
1073
1099
  {
1074
1100
  content: publishedPage.content,
@@ -1078,6 +1104,9 @@ const isAnyPagesDiff = computed(() => {
1078
1104
  metaTitle: publishedPage.metaTitle,
1079
1105
  metaDescription: publishedPage.metaDescription,
1080
1106
  structuredData: publishedPage.structuredData,
1107
+ postMetaTitle: publishedPage.postMetaTitle,
1108
+ postMetaDescription: publishedPage.postMetaDescription,
1109
+ postStructuredData: publishedPage.postStructuredData,
1081
1110
  },
1082
1111
  )) {
1083
1112
  return true
@@ -1596,7 +1625,7 @@ const pageSettingsUpdated = async (pageData) => {
1596
1625
  <edge-shad-button variant="destructive" class="text-white" @click="state.siteSettings = false">
1597
1626
  Cancel
1598
1627
  </edge-shad-button>
1599
- <edge-shad-button :disabled="slotProps.submitting" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1628
+ <edge-shad-button :disabled="slotProps.submitting || isJsonInvalid(slotProps.workingDoc?.structuredData)" type="submit" class=" bg-slate-800 hover:bg-slate-400 w-full">
1600
1629
  <Loader2 v-if="slotProps.submitting" class=" h-4 w-4 animate-spin" />
1601
1630
  Update
1602
1631
  </edge-shad-button>
@@ -534,11 +534,18 @@ onMounted(async () => {
534
534
  label="Meta Description"
535
535
  name="metaDescription"
536
536
  />
537
+ <div class="rounded-md border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
538
+ CMS tokens in double curly braces are replaced on the front end.
539
+ Example: <span v-pre class="font-semibold text-foreground">"{{cms-site}}"</span> for the site URL,
540
+ <span v-pre class="font-semibold text-foreground">"{{cms-url}}"</span> for the page URL, and
541
+ <span v-pre class="font-semibold text-foreground">"{{cms-logo}}"</span> for the logo URL. Keep the tokens intact.
542
+ </div>
537
543
  <edge-cms-code-editor
538
544
  v-model="props.settings.structuredData"
539
545
  title="Structured Data (JSON-LD)"
540
546
  language="json"
541
547
  name="structuredData"
548
+ validate-json
542
549
  height="300px"
543
550
  class="mb-4 w-full"
544
551
  />