nuxt-og-image 6.5.2 → 6.6.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.
Files changed (88) hide show
  1. package/dist/chunks/tw4.cjs +1 -1
  2. package/dist/chunks/tw4.mjs +1 -1
  3. package/dist/chunks/uno.cjs +1 -1
  4. package/dist/chunks/uno.mjs +1 -1
  5. package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
  6. package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
  7. package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
  8. package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
  9. package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
  10. package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
  11. package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
  12. package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
  13. package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
  14. package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
  15. package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
  16. package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
  17. package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
  18. package/dist/devtools/lib/og-image/keys.ts +8 -0
  19. package/dist/devtools/lib/og-image/og-image.ts +536 -0
  20. package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
  21. package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
  22. package/dist/devtools/lib/og-image/rpc.ts +73 -0
  23. package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
  24. package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
  25. package/dist/devtools/lib/og-image/shared.ts +30 -0
  26. package/dist/devtools/lib/og-image/templates.ts +9 -0
  27. package/dist/devtools/lib/og-image/types.ts +38 -0
  28. package/dist/devtools/lib/og-image/util/logic.ts +21 -0
  29. package/dist/devtools/nuxt.config.ts +6 -0
  30. package/dist/devtools/pages/og-image/debug.vue +32 -0
  31. package/dist/devtools/pages/og-image/docs.vue +3 -0
  32. package/dist/devtools/pages/og-image/index.vue +1682 -0
  33. package/dist/devtools/pages/og-image/templates.vue +150 -0
  34. package/dist/devtools/pages/og-image.vue +184 -0
  35. package/dist/module.cjs +1 -1
  36. package/dist/module.json +1 -1
  37. package/dist/module.mjs +1 -1
  38. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
  39. package/dist/runtime/server/og-image/core/style-attr.d.ts +8 -0
  40. package/dist/runtime/server/og-image/core/style-attr.js +34 -0
  41. package/dist/runtime/server/og-image/core/vnodes.d.ts +2 -1
  42. package/dist/runtime/server/og-image/core/vnodes.js +3 -27
  43. package/dist/shared/{nuxt-og-image.CgPzmzQY.cjs → nuxt-og-image.CfTPCtaS.cjs} +38 -24
  44. package/dist/shared/{nuxt-og-image.DGAMxBol.mjs → nuxt-og-image.DdbTs-xp.mjs} +37 -23
  45. package/package.json +17 -18
  46. package/dist/devtools/200.html +0 -1
  47. package/dist/devtools/404.html +0 -1
  48. package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
  49. package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
  50. package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
  51. package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
  52. package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
  53. package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
  54. package/dist/devtools/_nuxt/B9jrmesR.js +0 -1
  55. package/dist/devtools/_nuxt/BA-4cUNc.js +0 -1
  56. package/dist/devtools/_nuxt/BOEXnX7x.js +0 -3
  57. package/dist/devtools/_nuxt/BWKJ0Uxb.js +0 -30
  58. package/dist/devtools/_nuxt/Bdtz4ZK9.js +0 -4
  59. package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
  60. package/dist/devtools/_nuxt/C5jzcy9i.js +0 -1
  61. package/dist/devtools/_nuxt/CCTv7mmB.js +0 -1
  62. package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
  63. package/dist/devtools/_nuxt/C_JnDlx-.js +0 -1
  64. package/dist/devtools/_nuxt/CdmciVPJ.js +0 -1
  65. package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
  66. package/dist/devtools/_nuxt/CuJezOxb.js +0 -1
  67. package/dist/devtools/_nuxt/D4EkL0XJ.js +0 -6
  68. package/dist/devtools/_nuxt/DKaW7clv.js +0 -1
  69. package/dist/devtools/_nuxt/DSl3mlLY.js +0 -2
  70. package/dist/devtools/_nuxt/DYta7Mdi.js +0 -3
  71. package/dist/devtools/_nuxt/DevtoolsSection.CcOMr_vO.css +0 -1
  72. package/dist/devtools/_nuxt/DevtoolsSnippet.BhrTdbXn.css +0 -1
  73. package/dist/devtools/_nuxt/E8AZ6HoH.js +0 -1
  74. package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
  75. package/dist/devtools/_nuxt/O5eLyffU.js +0 -1
  76. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  77. package/dist/devtools/_nuxt/builds/meta/a36bc212-7179-4aa2-a534-6366229bd7b1.json +0 -1
  78. package/dist/devtools/_nuxt/entry.CcrXpo2y.css +0 -2
  79. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  80. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  81. package/dist/devtools/_nuxt/pages.Ci_xvfJ8.css +0 -1
  82. package/dist/devtools/_nuxt/renderer-select.COGJ4ZQe.css +0 -1
  83. package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
  84. package/dist/devtools/_nuxt/wMBdlVF-.js +0 -152
  85. package/dist/devtools/debug/index.html +0 -1
  86. package/dist/devtools/docs/index.html +0 -1
  87. package/dist/devtools/index.html +0 -1
  88. package/dist/devtools/templates/index.html +0 -1
@@ -0,0 +1,150 @@
1
+ <script lang="ts" setup>
2
+ import { withQuery } from 'ufo'
3
+ import { navigateTo } from '#imports'
4
+ import { useOgImage } from '../../lib/og-image/og-image'
5
+
6
+ const {
7
+ appComponents,
8
+ communityComponents,
9
+ activeComponentName,
10
+ src,
11
+ aspectRatio,
12
+ isLoading,
13
+ patchOptions,
14
+ } = useOgImage()
15
+
16
+ async function selectTemplate(componentName: string) {
17
+ patchOptions({ component: componentName })
18
+ await navigateTo('/')
19
+ }
20
+ </script>
21
+
22
+ <template>
23
+ <div class="templates-page animate-fade-up">
24
+ <!-- Loading state -->
25
+ <div v-if="isLoading" class="loading-container">
26
+ <div class="loading-spinner" />
27
+ <p class="loading-text">
28
+ Loading templates&#8230;
29
+ </p>
30
+ </div>
31
+
32
+ <!-- Content -->
33
+ <div v-else class="templates-content stagger-children">
34
+ <!-- Your Templates -->
35
+ <DevtoolsSection v-if="appComponents.length" icon="carbon:app" text="Your Templates">
36
+ <template #description>
37
+ Custom OG Image components in your project
38
+ </template>
39
+ <div class="template-grid">
40
+ <button
41
+ v-for="name in appComponents"
42
+ :key="name.pascalName"
43
+ class="template-item"
44
+ @click="selectTemplate(name.pascalName)"
45
+ >
46
+ <TemplateComponentPreview
47
+ :component="name"
48
+ :src="withQuery(src, { component: name.pascalName })"
49
+ :aspect-ratio="aspectRatio"
50
+ :active="name.pascalName === activeComponentName"
51
+ />
52
+ </button>
53
+ </div>
54
+ </DevtoolsSection>
55
+
56
+ <!-- Community Templates -->
57
+ <DevtoolsSection icon="carbon:user-multiple" text="Community Templates">
58
+ <template #description>
59
+ Pre-built templates you can use or eject
60
+ </template>
61
+ <div class="template-grid">
62
+ <button
63
+ v-for="name in communityComponents"
64
+ :key="name.pascalName"
65
+ class="template-item"
66
+ @click="selectTemplate(name.pascalName)"
67
+ >
68
+ <TemplateComponentPreview
69
+ :component="name"
70
+ :src="withQuery(src, { component: name.pascalName })"
71
+ :aspect-ratio="aspectRatio"
72
+ :active="name.pascalName === activeComponentName"
73
+ />
74
+ </button>
75
+ </div>
76
+ </DevtoolsSection>
77
+ </div>
78
+ </div>
79
+ </template>
80
+
81
+ <style scoped>
82
+ .templates-page {
83
+ padding-bottom: 2rem;
84
+ }
85
+
86
+ .loading-container {
87
+ display: flex;
88
+ flex-direction: column;
89
+ align-items: center;
90
+ justify-content: center;
91
+ min-height: 300px;
92
+ gap: 1rem;
93
+ }
94
+
95
+ .loading-spinner {
96
+ width: 2.5rem;
97
+ height: 2.5rem;
98
+ border: 2px solid var(--color-border);
99
+ border-top-color: var(--seo-green);
100
+ border-radius: 50%;
101
+ animation: spin 0.8s linear infinite;
102
+ }
103
+
104
+ @keyframes spin {
105
+ to {
106
+ transform: rotate(360deg);
107
+ }
108
+ }
109
+
110
+ .loading-text {
111
+ font-size: 0.875rem;
112
+ color: var(--color-text-muted);
113
+ }
114
+
115
+ .templates-content {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: 1.5rem;
119
+ }
120
+
121
+ .template-grid {
122
+ display: grid;
123
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
124
+ gap: 1rem;
125
+ }
126
+
127
+ @media (min-width: 640px) {
128
+ .template-grid {
129
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
130
+ gap: 1.25rem;
131
+ }
132
+ }
133
+
134
+ .template-item {
135
+ padding: 0;
136
+ background: none;
137
+ border: none;
138
+ text-align: left;
139
+ cursor: pointer;
140
+ transition: transform 150ms cubic-bezier(0.22, 1, 0.36, 1);
141
+ }
142
+
143
+ .template-item:hover {
144
+ transform: translateY(-2px);
145
+ }
146
+
147
+ .template-item:active {
148
+ transform: translateY(0);
149
+ }
150
+ </style>
@@ -0,0 +1,184 @@
1
+ <script lang="ts" setup>
2
+ import type { OgImageRuntimeConfig } from '../lib/og-image/runtime-types'
3
+ import type { GlobalDebugResponse, PathDebugResponse } from '../lib/og-image/types'
4
+ import defu from 'defu'
5
+ import { appFetch } from 'nuxtseo-layer-devtools/composables/rpc'
6
+ import { loadShiki } from 'nuxtseo-layer-devtools/composables/shiki'
7
+ import { path, productionUrl, query, refreshTime } from 'nuxtseo-layer-devtools/composables/state'
8
+ import { computed, provide, useAsyncData, useNuxtApp, useRoute, watch } from '#imports'
9
+ import AddComponentDialog from '../components/og-image/AddComponentDialog.vue'
10
+ import CreateOgImageDialog from '../components/og-image/CreateOgImageDialog.vue'
11
+ import RendererSelectModal from '../components/og-image/RendererSelectModal.vue'
12
+ import { GlobalDebugKey, PathDebugKey, PathDebugStatusKey, RefetchPathDebugKey } from '../lib/og-image/keys'
13
+ import { useOgImage } from '../lib/og-image/og-image'
14
+ import { encodeOgImageParams } from '../lib/og-image/shared/urlEncoding'
15
+ import { ogImageKey, options, optionsOverrides } from '../lib/og-image/util/logic'
16
+
17
+ const RE_IMAGE_EXT = /\.(png|jpeg|jpg|webp)$/
18
+
19
+ await loadShiki()
20
+
21
+ const nuxtApp = useNuxtApp()
22
+ nuxtApp.payload.data = nuxtApp.payload.data || {}
23
+
24
+ // @ts-expect-error untyped
25
+ const { data: globalDebug } = useAsyncData<GlobalDebugResponse>('og-image-global-debug', () => {
26
+ if (!appFetch.value)
27
+ return { runtimeConfig: {} as OgImageRuntimeConfig, componentNames: [] }
28
+ return appFetch.value('/_og/debug.json')
29
+ }, {
30
+ watch: [appFetch, refreshTime],
31
+ default: () => ({ runtimeConfig: {} as OgImageRuntimeConfig, componentNames: [] }),
32
+ })
33
+
34
+ // Read og:image URL from host document — already has correct component/params from defineOgImage
35
+ function getHostOgImageDebugUrl(): string | undefined {
36
+ try {
37
+ const doc = window.parent?.document
38
+ if (!doc)
39
+ return
40
+ const meta = doc.querySelector('meta[property="og:image"]') || doc.querySelector('meta[name="twitter:image"]')
41
+ const content = meta?.getAttribute('content')
42
+ if (!content?.includes('/_og/'))
43
+ return
44
+ return new URL(content).pathname.replace(RE_IMAGE_EXT, '.json')
45
+ }
46
+ catch {
47
+ // Cross-origin parent documents cannot be inspected; fall back to route options.
48
+ }
49
+ }
50
+
51
+ const { data: pathDebug, refresh: refreshPathDebug, status: pathDebugStatus } = useAsyncData<PathDebugResponse>('og-image-path-debug', async () => {
52
+ if (!appFetch.value)
53
+ return { extract: { options: [], socialPreview: { root: {}, images: [] } } }
54
+ let url = getHostOgImageDebugUrl()
55
+ if (!url) {
56
+ const params = defu(
57
+ { key: ogImageKey.value || 'og', _path: path.value, _query: query.value },
58
+ optionsOverrides.value,
59
+ options.value,
60
+ )
61
+ const encoded = encodeOgImageParams(params)
62
+ url = `/_og/d/${encoded || 'default'}.json`
63
+ }
64
+ return (appFetch.value(url) as Promise<PathDebugResponse>).catch((err: any): PathDebugResponse => ({
65
+ extract: { options: [], socialPreview: { root: {}, images: [] } },
66
+ fetchError: {
67
+ statusCode: err?.data?.statusCode || err?.statusCode,
68
+ message: err?.data?.stack?.[0] || err?.data?.message || err?.message || 'Unknown error',
69
+ stack: err?.data?.stack,
70
+ },
71
+ }))
72
+ }, {
73
+ watch: [appFetch, path, refreshTime, ogImageKey],
74
+ default: () => ({ extract: { options: [], socialPreview: { root: {}, images: [] } } }),
75
+ })
76
+
77
+ // Sync production URL from site config for production preview toggle
78
+ watch(globalDebug, (val) => {
79
+ // @ts-expect-error globalDebug type doesn't include siteConfigUrl
80
+ if (val?.siteConfigUrl)
81
+ // @ts-expect-error globalDebug type doesn't include siteConfigUrl
82
+ productionUrl.value = val.siteConfigUrl
83
+ }, { immediate: true })
84
+
85
+ // @ts-expect-error untyped
86
+ provide(GlobalDebugKey, globalDebug)
87
+ provide(PathDebugKey, pathDebug)
88
+ provide(PathDebugStatusKey, pathDebugStatus)
89
+ provide(RefetchPathDebugKey, refreshPathDebug)
90
+
91
+ const {
92
+ isDebugLoading,
93
+ error,
94
+ refreshSources,
95
+ resetProps,
96
+ } = useOgImage()
97
+
98
+ await resetProps(false)
99
+
100
+ const route = useRoute()
101
+ const currentTab = computed(() => {
102
+ const p = route.path
103
+ if (p === '/og-image/templates')
104
+ return 'templates'
105
+ if (p === '/og-image/debug')
106
+ return 'debug'
107
+ if (p === '/og-image/docs')
108
+ return 'docs'
109
+ return 'design'
110
+ })
111
+
112
+ const navItems = [
113
+ { value: 'design', to: '/og-image', icon: 'carbon:brush-freehand', label: 'Design', devOnly: false },
114
+ { value: 'templates', to: '/og-image/templates', icon: 'carbon:image', label: 'Templates', devOnly: false },
115
+ { value: 'debug', to: '/og-image/debug', icon: 'carbon:debug', label: 'Debug', devOnly: true },
116
+ { value: 'docs', to: '/og-image/docs', icon: 'carbon:book', label: 'Docs', devOnly: false },
117
+ ]
118
+
119
+ const runtimeVersion = computed(() => {
120
+ // @ts-expect-error untyped
121
+ return globalDebug.value?.runtimeConfig?.version || 'unknown'
122
+ })
123
+ </script>
124
+
125
+ <template>
126
+ <DevtoolsLayout
127
+ module-name="nuxt-og-image"
128
+ title="OG Image"
129
+ icon="carbon:image-search"
130
+ :version="runtimeVersion"
131
+ :nav-items="navItems"
132
+ github-url="https://github.com/nuxt-modules/og-image"
133
+ :loading="isDebugLoading || !!error"
134
+ :active-tab="currentTab"
135
+ @refresh="refreshSources"
136
+ >
137
+ <AddComponentDialog />
138
+ <CreateOgImageDialog />
139
+ <RendererSelectModal />
140
+ <NuxtPage />
141
+ </DevtoolsLayout>
142
+ </template>
143
+
144
+ <style>
145
+ /* Textarea */
146
+ textarea {
147
+ background: var(--color-surface-sunken);
148
+ border: 1px solid var(--color-border);
149
+ border-radius: var(--radius-md);
150
+ }
151
+
152
+ textarea:focus-visible {
153
+ border-color: var(--seo-green);
154
+ outline: none;
155
+ }
156
+
157
+ /* JSON Editor theme */
158
+ :root {
159
+ --jse-theme-color: var(--color-surface-elevated) !important;
160
+ --jse-text-color-inverse: var(--color-text-muted) !important;
161
+ --jse-theme-color-highlight: var(--color-surface-sunken) !important;
162
+ --jse-panel-background: var(--color-surface-elevated) !important;
163
+ --jse-background-color: var(--jse-panel-background) !important;
164
+ --jse-error-color: oklch(65% 0.2 25 / 0.3) !important;
165
+ --jse-main-border: none !important;
166
+ }
167
+
168
+ .dark,
169
+ .jse-theme-dark {
170
+ --jse-panel-background: var(--color-neutral-900) !important;
171
+ --jse-theme-color: var(--color-neutral-900) !important;
172
+ --jse-text-color-inverse: var(--color-neutral-300) !important;
173
+ --jse-main-border: none !important;
174
+ }
175
+
176
+ .jse-main {
177
+ min-height: 1em !important;
178
+ }
179
+
180
+ .jse-contents {
181
+ border-width: 0 !important;
182
+ border-radius: var(--radius-md) !important;
183
+ }
184
+ </style>
package/dist/module.cjs CHANGED
@@ -10,7 +10,7 @@ require('ohash');
10
10
  require('pathe');
11
11
  require('pkg-types');
12
12
  require('std-env');
13
- const module$1 = require('./shared/nuxt-og-image.CgPzmzQY.cjs');
13
+ const module$1 = require('./shared/nuxt-og-image.CfTPCtaS.cjs');
14
14
  require('nuxtseo-shared/kit');
15
15
  require('../dist/runtime/logger.js');
16
16
  require('node:crypto');
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.16.0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "6.5.2",
7
+ "version": "6.6.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -8,7 +8,7 @@ import 'ohash';
8
8
  import 'pathe';
9
9
  import 'pkg-types';
10
10
  import 'std-env';
11
- export { m as default } from './shared/nuxt-og-image.DGAMxBol.mjs';
11
+ export { m as default } from './shared/nuxt-og-image.DdbTs-xp.mjs';
12
12
  import 'nuxtseo-shared/kit';
13
13
  import '../dist/runtime/logger.js';
14
14
  import 'node:crypto';
@@ -120,8 +120,8 @@ export default defineTransformer([
120
120
  let src = node.props.src;
121
121
  if (src.startsWith("data:"))
122
122
  return;
123
- if (src.endsWith(".webp")) {
124
- logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the chromium renderer.", src);
123
+ if (ctx.renderer.name === "satori" && src.endsWith(".webp")) {
124
+ logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the takumi or browser renderer.", src);
125
125
  }
126
126
  const isRelative = src.startsWith("/");
127
127
  if (!isRelative)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Parse an inline `style="..."` attribute value into a camelCase property map
3
+ * suitable for handing to Satori / Takumi.
4
+ *
5
+ * Kept in a leaf module (no nitro / h3 imports) so it can be unit-tested
6
+ * directly without pulling the full runtime bundle.
7
+ */
8
+ export declare function parseStyleAttr(style: string | null | undefined): Record<string, any> | undefined;
@@ -0,0 +1,34 @@
1
+ import { splitCssDeclarations } from "../utils/css.js";
2
+ const RE_KEBAB_SEGMENT = /-([a-z])/g;
3
+ const RE_CSS_QUOTES = /^['"](.+)['"]$/;
4
+ const RE_IMPORTANT = /\s*!important\s*$/;
5
+ const RE_HTML_ENTITY = /&(?:amp|quot|#39|#x27);/g;
6
+ const HTML_ENTITY_MAP = {
7
+ "&amp;": "&",
8
+ "&quot;": '"',
9
+ "&#39;": "'",
10
+ "&#x27;": "'"
11
+ };
12
+ function camelCase(str) {
13
+ return str.replace(RE_KEBAB_SEGMENT, (_, c) => c.toUpperCase());
14
+ }
15
+ function decodeHtmlEntities(value) {
16
+ return value.replace(RE_HTML_ENTITY, (m) => HTML_ENTITY_MAP[m] ?? m);
17
+ }
18
+ export function parseStyleAttr(style) {
19
+ if (!style)
20
+ return void 0;
21
+ const result = {};
22
+ for (const decl of splitCssDeclarations(decodeHtmlEntities(style))) {
23
+ const colonIdx = decl.indexOf(":");
24
+ if (colonIdx === -1)
25
+ continue;
26
+ const prop = decl.slice(0, colonIdx).trim();
27
+ const val = decl.slice(colonIdx + 1).trim();
28
+ if (prop && val) {
29
+ const cleanVal = val.replace(RE_IMPORTANT, "");
30
+ result[camelCase(prop)] = prop === "font-family" ? cleanVal.replace(RE_CSS_QUOTES, "$1") : cleanVal;
31
+ }
32
+ }
33
+ return Object.keys(result).length ? result : void 0;
34
+ }
@@ -1,5 +1,7 @@
1
1
  import type { ElementNode } from 'ultrahtml';
2
2
  import type { OgImageRenderEventContext, VNode } from '../../../types.js';
3
+ import { parseStyleAttr } from './style-attr.js';
4
+ export { parseStyleAttr };
3
5
  export declare const SVG_CAMEL_ATTR_VALUES: Set<string>;
4
6
  /**
5
7
  * Resolve an SVG element's width or height from multiple sources:
@@ -11,7 +13,6 @@ export declare const SVG_CAMEL_ATTR_VALUES: Set<string>;
11
13
  * can fall back to ancestor or renderer-specific resolution.
12
14
  */
13
15
  export declare function resolveSvgDimension(props: Record<string, any>, style: Record<string, any> | undefined, key: 'width' | 'height'): number | undefined;
14
- export declare function parseStyleAttr(style: string | null | undefined): Record<string, any> | undefined;
15
16
  export declare function elementToVNode(el: ElementNode): VNode;
16
17
  export declare function htmlToVNode(html: string): VNode;
17
18
  export declare function warnUnsupportedSvgElements(vnode: VNode, component?: string): void;
@@ -1,22 +1,16 @@
1
1
  import { ELEMENT_NODE, parse, TEXT_NODE } from "ultrahtml";
2
2
  import { querySelector } from "ultrahtml/selector";
3
- import { decodeHtml, htmlDecodeQuotes } from "../../util/encoding.js";
3
+ import { decodeHtml } from "../../util/encoding.js";
4
4
  import { fetchIsland } from "../../util/kit.js";
5
5
  import { logger } from "../../util/logger.js";
6
- import { splitCssDeclarations } from "../utils/css.js";
7
6
  import { walkTree } from "./plugins.js";
8
7
  import encoding from "./plugins/encoding.js";
9
8
  import imageSrc from "./plugins/imageSrc.js";
10
9
  import styleDirectives from "./plugins/styleDirectives.js";
10
+ import { parseStyleAttr } from "./style-attr.js";
11
11
  import { applyEmojis } from "./transforms/emojis/index.js";
12
- const RE_KEBAB_SEGMENT = /-([a-z])/g;
13
- const RE_AMP_ENTITY = /&amp;/g;
14
- const RE_CSS_QUOTES = /^['"](.+)['"]$/;
15
- const RE_IMPORTANT = /\s*!important\s*$/;
12
+ export { parseStyleAttr };
16
13
  const RE_BODY_CONTENT = /<body>([\s\S]*)<\/body>/;
17
- function camelCase(str) {
18
- return str.replace(RE_KEBAB_SEGMENT, (_, c) => c.toUpperCase());
19
- }
20
14
  const SVG_CAMEL_ATTRS = {
21
15
  viewbox: "viewBox",
22
16
  preserveaspectratio: "preserveAspectRatio",
@@ -94,23 +88,6 @@ export function resolveSvgDimension(props, style, key) {
94
88
  }
95
89
  }
96
90
  }
97
- export function parseStyleAttr(style) {
98
- if (!style)
99
- return void 0;
100
- const result = {};
101
- for (const decl of splitCssDeclarations(style.replace(RE_AMP_ENTITY, "&"))) {
102
- const colonIdx = decl.indexOf(":");
103
- if (colonIdx === -1)
104
- continue;
105
- const prop = decl.slice(0, colonIdx).trim();
106
- const val = decl.slice(colonIdx + 1).trim();
107
- if (prop && val) {
108
- const cleanVal = val.replace(RE_IMPORTANT, "");
109
- result[camelCase(prop)] = prop === "font-family" ? cleanVal.replace(RE_CSS_QUOTES, "$1") : cleanVal;
110
- }
111
- }
112
- return Object.keys(result).length ? result : void 0;
113
- }
114
91
  export function elementToVNode(el) {
115
92
  const props = {};
116
93
  const { style, ...attrs } = el.attributes;
@@ -180,7 +157,6 @@ export async function createVNodes(ctx, options) {
180
157
  if (!html) {
181
158
  const islandTimeout = ctx.runtimeConfig.security?.renderTimeout ?? 15e3;
182
159
  const island = await ctx.timings.measure("island-fetch", () => fetchIsland(ctx.e, ctx.options.component, typeof ctx.options.props !== "undefined" ? ctx.options.props : ctx.options, islandTimeout));
183
- island.html = htmlDecodeQuotes(island.html);
184
160
  await applyEmojis(ctx, island);
185
161
  html = island.html;
186
162
  if (html?.includes("<body>")) {
@@ -3125,7 +3125,6 @@ const RE_SVG_ID_ATTR = /\bid="([^"]+)"/g;
3125
3125
  const RE_SVG_VIEWBOX = /viewBox="([^"]*)"/;
3126
3126
  const RE_SVG_BODY = /<svg[^>]*>([\s\S]*)<\/svg>/;
3127
3127
  const RE_OG_IMAGE_QUERY$1 = /\?og-image(?:-depth=\d+)?$/;
3128
- const RE_TEMPLATE_CONTENT = /<template>([\s\S]*?)<\/template>/;
3129
3128
  const RE_TEXT_BETWEEN_TAGS = />([^<]*)</g;
3130
3129
  const RE_SPECIAL_REGEX_CHARS = /[.*+?^${}()|[\]\\]/g;
3131
3130
  const RE_WRAPPER_DIV_START = /^<div>/;
@@ -3137,7 +3136,6 @@ const RE_DATA_ATTR = /\bdata-([\w-]+)="([^"]*)"/g;
3137
3136
  const RE_DYNAMIC_DATA_THEME = /\s:data-theme="[^"]+"/;
3138
3137
  const RE_COLOR_ATTR_NAME = /^(?:color|fill|stroke|flood-color|lighting-color|stop-color)$/;
3139
3138
  const RE_NON_STYLE_VAR_ATTR = /\b(?!style|class)([a-zA-Z-]+)="([^"]*var\(--[^"]+)"/g;
3140
- const RE_QUOTED_ATTR = /^([a-z-]+)="(.*)"$/i;
3141
3139
  const RE_COLOR_PROP_NAME = /color|fill|stroke|background|border|outline|shadow|accent|caret/;
3142
3140
  const RE_STYLE_VAR_ATTR = /\bstyle="([^"]*var\(--[^"]+)"/g;
3143
3141
  const RE_INLINE_STYLE = /\bstyle="([^"]*)"/g;
@@ -3187,11 +3185,11 @@ function buildIconSvg(iconData, defaultWidth, defaultHeight, attrs) {
3187
3185
  existingStyle = value;
3188
3186
  continue;
3189
3187
  }
3190
- filteredAttrs.push(`${key}="${value}"`);
3188
+ filteredAttrs.push(`${key}="${escapeAttrValue(value)}"`);
3191
3189
  }
3192
3190
  const attrsStr = filteredAttrs.length > 0 ? ` ${filteredAttrs.join(" ")}` : "";
3193
3191
  const mergedStyle = existingStyle ? `display:flex; ${existingStyle}` : "display:flex";
3194
- let svg = `<span${attrsStr} style="${mergedStyle}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="100%" height="100%" fill="currentColor">${body}</svg></span>`;
3192
+ let svg = `<span${attrsStr} style="${escapeAttrValue(mergedStyle)}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="100%" height="100%" fill="currentColor">${body}</svg></span>`;
3195
3193
  svg = makeIdsUnique(svg);
3196
3194
  return svg;
3197
3195
  }
@@ -3343,6 +3341,21 @@ function mergeThemePairs(classes, light, dark) {
3343
3341
  }
3344
3342
  return result;
3345
3343
  }
3344
+ function getOuterTemplateBounds(code) {
3345
+ let descriptor;
3346
+ try {
3347
+ descriptor = compilerSfc.parse(code).descriptor;
3348
+ } catch {
3349
+ return void 0;
3350
+ }
3351
+ const template = descriptor.template;
3352
+ if (!template)
3353
+ return void 0;
3354
+ return {
3355
+ templateStart: template.loc.start.offset,
3356
+ templateEnd: template.loc.end.offset
3357
+ };
3358
+ }
3346
3359
  const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3347
3360
  let emojiIcons = null;
3348
3361
  return {
@@ -3359,12 +3372,11 @@ const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3359
3372
  const id = rawId.replace(RE_OG_IMAGE_QUERY$1, "");
3360
3373
  if (rawId.includes("?og-image"))
3361
3374
  options.onNestedTransform?.(id);
3362
- const templateMatch = code.match(RE_TEMPLATE_CONTENT);
3363
- if (!templateMatch)
3375
+ const bounds = getOuterTemplateBounds(code);
3376
+ if (!bounds)
3364
3377
  return;
3365
3378
  const s = new MagicString__default(code);
3366
- const templateStart = code.indexOf("<template>") + "<template>".length;
3367
- const templateEnd = code.indexOf("</template>");
3379
+ const { templateStart, templateEnd } = bounds;
3368
3380
  let template = code.slice(templateStart, templateEnd);
3369
3381
  let hasChanges = false;
3370
3382
  if (options.emojiSet && emojiUtils_js.RE_MATCH_EMOJIS.test(template)) {
@@ -3551,10 +3563,11 @@ const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3551
3563
  }
3552
3564
  });
3553
3565
  if (result) {
3554
- const newTemplateStart = result.code.indexOf("<template>") + "<template>".length;
3555
- const newTemplateEnd = result.code.indexOf("</template>");
3556
- template = result.code.slice(newTemplateStart, newTemplateEnd);
3557
- hasChanges = true;
3566
+ const newBounds = getOuterTemplateBounds(result.code);
3567
+ if (newBounds) {
3568
+ template = result.code.slice(newBounds.templateStart, newBounds.templateEnd);
3569
+ hasChanges = true;
3570
+ }
3558
3571
  }
3559
3572
  } catch (err) {
3560
3573
  const componentName = id.split("/").pop();
@@ -3578,16 +3591,16 @@ const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3578
3591
  template.replace(RE_NON_STYLE_VAR_ATTR, (match, attr, value) => {
3579
3592
  const resolved = resolveCssVars(value, vars);
3580
3593
  if (resolved !== value) {
3581
- replacements.push({ from: match, to: `${attr}="${resolved}"` });
3594
+ replacements.push({ from: match, to: "", attr, value: resolved });
3582
3595
  }
3583
3596
  return match;
3584
3597
  });
3585
3598
  for (const r of replacements) {
3586
- const attrMatch = r.to.match(RE_QUOTED_ATTR);
3587
- if (attrMatch?.[1] && attrMatch[2] !== void 0 && RE_COLOR_ATTR_NAME.test(attrMatch[1])) {
3588
- const downleveled = await downlevelColor(attrMatch[1], attrMatch[2]);
3589
- r.to = `${attrMatch[1]}="${downleveled}"`;
3599
+ let value = r.value;
3600
+ if (RE_COLOR_ATTR_NAME.test(r.attr)) {
3601
+ value = await downlevelColor(r.attr, value);
3590
3602
  }
3603
+ r.to = `${r.attr}="${escapeAttrValue(value)}"`;
3591
3604
  template = template.replace(r.from, r.to);
3592
3605
  hasChanges = true;
3593
3606
  }
@@ -3596,12 +3609,11 @@ const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3596
3609
  template.replace(RE_STYLE_VAR_ATTR, (match, styleValue) => {
3597
3610
  const resolved = resolveCssVars(styleValue, vars);
3598
3611
  if (resolved !== styleValue)
3599
- styleReplacements.push({ from: match, to: `style="${resolved}"` });
3612
+ styleReplacements.push({ from: match, content: resolved });
3600
3613
  return match;
3601
3614
  });
3602
3615
  for (const r of styleReplacements) {
3603
- const styleContent = r.to.slice('style="'.length, -1);
3604
- const declarations = styleContent.split(";").filter(Boolean);
3616
+ const declarations = r.content.split(";").filter(Boolean);
3605
3617
  const downleveled = [];
3606
3618
  for (const decl of declarations) {
3607
3619
  const colonIdx = decl.indexOf(":");
@@ -3618,8 +3630,8 @@ const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3618
3630
  }
3619
3631
  downleveled.push(`${prop}: ${value}`);
3620
3632
  }
3621
- r.to = `style="${downleveled.join("; ")}"`;
3622
- template = template.replace(r.from, r.to);
3633
+ const to = `style="${escapeAttrValue(downleveled.join("; "))}"`;
3634
+ template = template.replace(r.from, to);
3623
3635
  hasChanges = true;
3624
3636
  }
3625
3637
  RE_DYNAMIC_STYLE_VAR.lastIndex = 0;
@@ -3719,7 +3731,9 @@ const AssetTransformPlugin = unplugin.createUnplugin((options) => {
3719
3731
  existingProps[prop.trim()] = value;
3720
3732
  }
3721
3733
  const merged = { ...matchedStyles, ...existingProps };
3722
- el.attributes.style = Object.entries(merged).map(([p, v]) => `${p}:${v}`).join(";");
3734
+ el.attributes.style = escapeAttrValue(
3735
+ Object.entries(merged).map(([p, v]) => `${p}:${v}`).join(";")
3736
+ );
3723
3737
  const remaining = elClasses.filter((c) => !consumedClasses.has(c));
3724
3738
  if (remaining.length > 0)
3725
3739
  el.attributes.class = remaining.join(" ");
@@ -4699,7 +4713,7 @@ const module$1 = kit.defineNuxtModule({
4699
4713
  await onUpgrade(nuxt, options, previousVersion);
4700
4714
  },
4701
4715
  async setup(config, nuxt) {
4702
- const _resolver = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/nuxt-og-image.CgPzmzQY.cjs', document.baseURI).href)));
4716
+ const _resolver = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/nuxt-og-image.CfTPCtaS.cjs', document.baseURI).href)));
4703
4717
  const fixSharedPath = (p) => {
4704
4718
  return p.replace(/\/shared\/runtime(\/|$)/, "/runtime$1").replace(/\/shared\/client(\/|$)/, "/client$1").replace(/\/shared\/devtools(\/|$)/, "/devtools$1");
4705
4719
  };