@duffcloudservices/cms 0.5.1 → 0.7.1
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/dist/chunk-FUNIALH6.js +1003 -0
- package/dist/chunk-FUNIALH6.js.map +1 -0
- package/dist/chunk-KCWMS7P4.js +3 -0
- package/dist/chunk-KCWMS7P4.js.map +1 -0
- package/dist/index.d.ts +4 -195
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +72 -28
- package/dist/plugins/index.js +221 -12
- package/dist/plugins/index.js.map +1 -1
- package/dist/seo/index.d.ts +248 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/vitepressTransform-y8Ru9elj.d.ts +971 -0
- package/package.json +8 -3
- package/src/components/ManagedImage.test.ts +75 -0
- package/src/components/ManagedImage.vue +19 -3
- package/src/components/ResponsiveImage.test.ts +47 -0
- package/src/components/ResponsiveImage.vue +15 -3
- package/dist/chunk-JJK7OGC2.js +0 -459
- package/dist/chunk-JJK7OGC2.js.map +0 -1
- package/dist/vitepressTransform-D9P9D7So.d.ts +0 -479
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duffcloudservices/cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Vue 3 composables and Vite plugins for DCS CMS integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/editor/editorBridge.d.ts",
|
|
17
17
|
"import": "./dist/editor/editorBridge.js"
|
|
18
18
|
},
|
|
19
|
+
"./seo": {
|
|
20
|
+
"types": "./dist/seo/index.d.ts",
|
|
21
|
+
"import": "./dist/seo/index.js"
|
|
22
|
+
},
|
|
19
23
|
"./components": {
|
|
20
24
|
"import": "./src/components/PreviewRibbon.vue"
|
|
21
25
|
},
|
|
@@ -57,6 +61,7 @@
|
|
|
57
61
|
"@types/js-yaml": "^4.0.9",
|
|
58
62
|
"@types/markdown-it": "^14.1.0",
|
|
59
63
|
"@types/node": "^20.11.0",
|
|
64
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
60
65
|
"@vue/test-utils": "^2.4.0",
|
|
61
66
|
"markdown-it": "^14.0.0",
|
|
62
67
|
"tsup": "^8.0.0",
|
|
@@ -78,11 +83,11 @@
|
|
|
78
83
|
"license": "MIT",
|
|
79
84
|
"repository": {
|
|
80
85
|
"type": "git",
|
|
81
|
-
"url": "https://github.com/
|
|
86
|
+
"url": "https://github.com/NateDuff/dcs"
|
|
82
87
|
},
|
|
83
88
|
"homepage": "https://portal.duffcloudservices.com",
|
|
84
89
|
"bugs": {
|
|
85
|
-
"url": "https://github.com/
|
|
90
|
+
"url": "https://github.com/NateDuff/dcs/issues"
|
|
86
91
|
},
|
|
87
92
|
"engines": {
|
|
88
93
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import ManagedImage from './ManagedImage.vue'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A CDN URL whose UUID/extension triggers responsive variant resolution
|
|
7
|
+
* (so a `<picture>` with `<source>` renders by default).
|
|
8
|
+
*/
|
|
9
|
+
const ORIGINAL = 'https://files.duffcloudservices.com/kept/assets/hero/abc-123.jpg'
|
|
10
|
+
|
|
11
|
+
describe('ManagedImage self-healing fallback', () => {
|
|
12
|
+
it('renders a <picture> with <source> for a CDN image by default', () => {
|
|
13
|
+
const wrapper = mount(ManagedImage, {
|
|
14
|
+
props: {
|
|
15
|
+
pageSlug: 'home',
|
|
16
|
+
imageKey: 'hero.image',
|
|
17
|
+
fallbackSrc: ORIGINAL,
|
|
18
|
+
fallbackAlt: 'Hero',
|
|
19
|
+
context: 'hero',
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(wrapper.find('picture').exists()).toBe(true)
|
|
24
|
+
expect(wrapper.find('source').exists()).toBe(true)
|
|
25
|
+
// Default <img> src points at a generated variant, not the original.
|
|
26
|
+
expect(wrapper.find('img').attributes('src')).toContain('abc-123-md.webp')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('swaps to the original and removes <source> when the variant errors', async () => {
|
|
30
|
+
const wrapper = mount(ManagedImage, {
|
|
31
|
+
props: {
|
|
32
|
+
pageSlug: 'home',
|
|
33
|
+
imageKey: 'hero.image',
|
|
34
|
+
fallbackSrc: ORIGINAL,
|
|
35
|
+
fallbackAlt: 'Hero',
|
|
36
|
+
context: 'hero',
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Sanity: starts as a <picture> with variant <source>.
|
|
41
|
+
expect(wrapper.find('picture').exists()).toBe(true)
|
|
42
|
+
expect(wrapper.find('source').exists()).toBe(true)
|
|
43
|
+
|
|
44
|
+
// The 404'd fit-variant fires `error`.
|
|
45
|
+
await wrapper.find('img').trigger('error')
|
|
46
|
+
|
|
47
|
+
// The <picture>/<source> branch must drop out — a <picture> does NOT
|
|
48
|
+
// re-resolve after a <source> 404s, so the variant set has to be removed.
|
|
49
|
+
expect(wrapper.find('picture').exists()).toBe(false)
|
|
50
|
+
expect(wrapper.find('source').exists()).toBe(false)
|
|
51
|
+
|
|
52
|
+
// A plain <img> now renders the ORIGINAL url.
|
|
53
|
+
const img = wrapper.find('img')
|
|
54
|
+
expect(img.exists()).toBe(true)
|
|
55
|
+
expect(img.attributes('src')).toBe(ORIGINAL)
|
|
56
|
+
// The original is still exposed for the visual editor.
|
|
57
|
+
expect(img.attributes('data-dcs-image-url')).toBe(ORIGINAL)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('renders a plain <img> at the original when `original` is set (no variants)', () => {
|
|
61
|
+
const wrapper = mount(ManagedImage, {
|
|
62
|
+
props: {
|
|
63
|
+
pageSlug: 'home',
|
|
64
|
+
imageKey: 'hero.image',
|
|
65
|
+
fallbackSrc: ORIGINAL,
|
|
66
|
+
fallbackAlt: 'Hero',
|
|
67
|
+
context: 'hero',
|
|
68
|
+
original: true,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(wrapper.find('picture').exists()).toBe(false)
|
|
73
|
+
expect(wrapper.find('img').attributes('src')).toBe(ORIGINAL)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, useAttrs } from 'vue'
|
|
2
|
+
import { computed, ref, useAttrs } from 'vue'
|
|
3
3
|
import { useResponsiveImage } from '../composables/useResponsiveImage'
|
|
4
4
|
import { useTextContent } from '../composables/useTextContent'
|
|
5
5
|
import type { ImageContext } from '@duffcloudservices/cms-core'
|
|
@@ -42,9 +42,25 @@ const image = useResponsiveImage({
|
|
|
42
42
|
original: () => props.original,
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
// Self-heal: a fit-variant (`-md`/`-lg`) may legitimately be absent when the
|
|
46
|
+
// source is smaller than the target dimension (see internal/imaging variants.go,
|
|
47
|
+
// which skips variants by DIMENSION). When the rendered variant 404s, fall back
|
|
48
|
+
// to the ORIGINAL url and drop the `<picture>`/`<source>` branch — a `<picture>`
|
|
49
|
+
// does NOT re-resolve after a `<source>` 404s, so the variant set must be removed,
|
|
50
|
+
// not just the `<img>` src swapped.
|
|
51
|
+
const failed = ref(false)
|
|
52
|
+
function onVariantError() {
|
|
53
|
+
if (!failed.value) {
|
|
54
|
+
failed.value = true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
const imgAttrs = computed(() => ({
|
|
46
59
|
...image.imgProps,
|
|
47
60
|
...attrs,
|
|
61
|
+
// When a variant has failed, render the original (also exposed as
|
|
62
|
+
// data-dcs-image-url for the visual editor) instead of the 404'd variant.
|
|
63
|
+
...(failed.value ? { src: imageSrc.value } : {}),
|
|
48
64
|
'data-dcs-image-key': props.imageKey,
|
|
49
65
|
'data-dcs-image-url': imageSrc.value,
|
|
50
66
|
'data-dcs-image-alt': imageAlt.value,
|
|
@@ -52,7 +68,7 @@ const imgAttrs = computed(() => ({
|
|
|
52
68
|
</script>
|
|
53
69
|
|
|
54
70
|
<template>
|
|
55
|
-
<picture v-if="image.hasVariants && !original">
|
|
71
|
+
<picture v-if="image.hasVariants && !original && !failed">
|
|
56
72
|
<source
|
|
57
73
|
v-for="source in image.sources"
|
|
58
74
|
:key="source.type"
|
|
@@ -60,7 +76,7 @@ const imgAttrs = computed(() => ({
|
|
|
60
76
|
:type="source.type"
|
|
61
77
|
:sizes="source.sizes"
|
|
62
78
|
/>
|
|
63
|
-
<img v-bind="imgAttrs" />
|
|
79
|
+
<img v-bind="imgAttrs" @error="onVariantError" />
|
|
64
80
|
</picture>
|
|
65
81
|
<img v-else v-bind="imgAttrs" />
|
|
66
82
|
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import ResponsiveImage from './ResponsiveImage.vue'
|
|
4
|
+
|
|
5
|
+
const ORIGINAL = 'https://files.duffcloudservices.com/kept/assets/hero/abc-123.jpg'
|
|
6
|
+
|
|
7
|
+
describe('ResponsiveImage self-healing fallback', () => {
|
|
8
|
+
it('renders a <picture> with <source> for a CDN image by default', () => {
|
|
9
|
+
const wrapper = mount(ResponsiveImage, {
|
|
10
|
+
props: { src: ORIGINAL, alt: 'Hero', context: 'hero' },
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
expect(wrapper.find('picture').exists()).toBe(true)
|
|
14
|
+
expect(wrapper.find('source').exists()).toBe(true)
|
|
15
|
+
expect(wrapper.find('img').attributes('src')).toContain('abc-123-md.webp')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('swaps to props.src and removes <source> when the variant errors', async () => {
|
|
19
|
+
const wrapper = mount(ResponsiveImage, {
|
|
20
|
+
props: { src: ORIGINAL, alt: 'Hero', context: 'hero', class: 'object-cover' },
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(wrapper.find('picture').exists()).toBe(true)
|
|
24
|
+
expect(wrapper.find('source').exists()).toBe(true)
|
|
25
|
+
|
|
26
|
+
await wrapper.find('img').trigger('error')
|
|
27
|
+
|
|
28
|
+
// <picture>/<source> must be neutralized so the original can display.
|
|
29
|
+
expect(wrapper.find('picture').exists()).toBe(false)
|
|
30
|
+
expect(wrapper.find('source').exists()).toBe(false)
|
|
31
|
+
|
|
32
|
+
const img = wrapper.find('img')
|
|
33
|
+
expect(img.exists()).toBe(true)
|
|
34
|
+
expect(img.attributes('src')).toBe(ORIGINAL)
|
|
35
|
+
// The class binding survives the fallback render.
|
|
36
|
+
expect(img.classes()).toContain('object-cover')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders a plain <img> at props.src when `original` is set', () => {
|
|
40
|
+
const wrapper = mount(ResponsiveImage, {
|
|
41
|
+
props: { src: ORIGINAL, alt: 'Hero', context: 'hero', original: true },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(wrapper.find('picture').exists()).toBe(false)
|
|
45
|
+
expect(wrapper.find('img').attributes('src')).toBe(ORIGINAL)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* />
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
import { computed, useAttrs } from 'vue'
|
|
16
|
+
import { computed, ref, useAttrs } from 'vue'
|
|
17
17
|
import { useResponsiveImage } from '../composables/useResponsiveImage'
|
|
18
18
|
import type { ImageContext } from '@duffcloudservices/cms-core'
|
|
19
19
|
|
|
@@ -42,15 +42,27 @@ const image = useResponsiveImage({
|
|
|
42
42
|
original: () => props.original,
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
// Self-heal: when the rendered fit-variant (`-md`/`-lg`) 404s — legitimately
|
|
46
|
+
// absent for sources smaller than the target dimension — fall back to the
|
|
47
|
+
// ORIGINAL `props.src` and drop the `<picture>`/`<source>` branch. A `<picture>`
|
|
48
|
+
// does NOT re-resolve after a `<source>` 404s, so the variant set must be removed.
|
|
49
|
+
const failed = ref(false)
|
|
50
|
+
function onVariantError() {
|
|
51
|
+
if (!failed.value) {
|
|
52
|
+
failed.value = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
const attrs = useAttrs()
|
|
46
57
|
const imgAttrs = computed(() => ({
|
|
47
58
|
...image.imgProps,
|
|
48
59
|
...attrs,
|
|
60
|
+
...(failed.value ? { src: props.src } : {}),
|
|
49
61
|
}))
|
|
50
62
|
</script>
|
|
51
63
|
|
|
52
64
|
<template>
|
|
53
|
-
<picture v-if="image.hasVariants && !original">
|
|
65
|
+
<picture v-if="image.hasVariants && !original && !failed">
|
|
54
66
|
<source
|
|
55
67
|
v-for="source in image.sources"
|
|
56
68
|
:key="source.type"
|
|
@@ -58,7 +70,7 @@ const imgAttrs = computed(() => ({
|
|
|
58
70
|
:type="source.type"
|
|
59
71
|
:sizes="source.sizes"
|
|
60
72
|
/>
|
|
61
|
-
<img v-bind="imgAttrs" :class="props.class" />
|
|
73
|
+
<img v-bind="imgAttrs" :class="props.class" @error="onVariantError" />
|
|
62
74
|
</picture>
|
|
63
75
|
<img v-else v-bind="imgAttrs" :class="props.class" />
|
|
64
76
|
</template>
|