nuxt-og-image 6.5.3 → 6.7.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 (108) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/tw4.cjs +7 -7
  3. package/dist/chunks/tw4.mjs +7 -7
  4. package/dist/chunks/uno.cjs +6 -6
  5. package/dist/chunks/uno.mjs +6 -6
  6. package/dist/cli.cjs +12 -5
  7. package/dist/cli.mjs +10 -3
  8. package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
  9. package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
  10. package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
  11. package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
  12. package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
  13. package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
  14. package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
  15. package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
  16. package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
  17. package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
  18. package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
  19. package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
  20. package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
  21. package/dist/devtools/lib/og-image/keys.ts +8 -0
  22. package/dist/devtools/lib/og-image/og-image.ts +536 -0
  23. package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
  24. package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
  25. package/dist/devtools/lib/og-image/rpc.ts +73 -0
  26. package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
  27. package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
  28. package/dist/devtools/lib/og-image/shared.ts +30 -0
  29. package/dist/devtools/lib/og-image/templates.ts +9 -0
  30. package/dist/devtools/lib/og-image/types.ts +38 -0
  31. package/dist/devtools/lib/og-image/util/logic.ts +21 -0
  32. package/dist/devtools/nuxt.config.ts +6 -0
  33. package/dist/devtools/pages/og-image/debug.vue +32 -0
  34. package/dist/devtools/pages/og-image/docs.vue +3 -0
  35. package/dist/devtools/pages/og-image/index.vue +1682 -0
  36. package/dist/devtools/pages/og-image/templates.vue +150 -0
  37. package/dist/devtools/pages/og-image.vue +184 -0
  38. package/dist/module.cjs +5 -5
  39. package/dist/module.d.cts +5 -3
  40. package/dist/module.d.mts +5 -3
  41. package/dist/module.d.ts +5 -3
  42. package/dist/module.json +1 -1
  43. package/dist/module.mjs +5 -5
  44. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
  45. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +6 -1
  46. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +13 -6
  47. package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +36 -0
  48. package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +47 -0
  49. package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
  50. package/dist/runtime/server/og-image/bindings/font-assets/node.js +14 -7
  51. package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +7 -1
  52. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
  53. package/dist/runtime/server/og-image/fonts.d.ts +4 -0
  54. package/dist/runtime/server/og-image/fonts.js +21 -5
  55. package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
  56. package/dist/runtime/server/og-image/takumi/renderer.js +61 -19
  57. package/dist/runtime/server/util/ssrf.d.ts +10 -0
  58. package/dist/runtime/server/util/ssrf.js +9 -1
  59. package/dist/runtime/types.d.ts +13 -0
  60. package/dist/shared/{nuxt-og-image.DdbTs-xp.mjs → nuxt-og-image.CPSxZhVO.mjs} +122 -496
  61. package/dist/shared/nuxt-og-image.DM30kcDz.mjs +660 -0
  62. package/dist/shared/nuxt-og-image.RI9DgjgV.cjs +689 -0
  63. package/dist/shared/{nuxt-og-image.CfTPCtaS.cjs → nuxt-og-image.yDJBWXBt.cjs} +181 -555
  64. package/package.json +48 -45
  65. package/dist/devtools/200.html +0 -1
  66. package/dist/devtools/404.html +0 -1
  67. package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
  68. package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
  69. package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
  70. package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
  71. package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
  72. package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
  73. package/dist/devtools/_nuxt/BZ_CG9ll.js +0 -30
  74. package/dist/devtools/_nuxt/BdMalyEU.js +0 -4
  75. package/dist/devtools/_nuxt/BhkGaWEe.js +0 -3
  76. package/dist/devtools/_nuxt/Bm4Q-nTW.js +0 -3
  77. package/dist/devtools/_nuxt/BsRKQH5X.js +0 -1
  78. package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
  79. package/dist/devtools/_nuxt/CBcJg2GG.js +0 -2
  80. package/dist/devtools/_nuxt/CDaXKRRu.js +0 -1
  81. package/dist/devtools/_nuxt/CM-t-6ZT.js +0 -1
  82. package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
  83. package/dist/devtools/_nuxt/CXAM78_A.js +0 -1
  84. package/dist/devtools/_nuxt/CYDI7SLa.js +0 -1
  85. package/dist/devtools/_nuxt/CkbDfutK.js +0 -152
  86. package/dist/devtools/_nuxt/Co8Tk_C2.js +0 -1
  87. package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
  88. package/dist/devtools/_nuxt/D49I50CU.js +0 -1
  89. package/dist/devtools/_nuxt/DevtoolsSection.DkT2bJJh.css +0 -1
  90. package/dist/devtools/_nuxt/DevtoolsSnippet.zV_SDojn.css +0 -1
  91. package/dist/devtools/_nuxt/HWyD49jH.js +0 -1
  92. package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
  93. package/dist/devtools/_nuxt/PONEy9N-.js +0 -1
  94. package/dist/devtools/_nuxt/builds/latest.json +0 -1
  95. package/dist/devtools/_nuxt/builds/meta/0f3dbc15-8308-4956-81b9-5507cac7018c.json +0 -1
  96. package/dist/devtools/_nuxt/entry.D7u2asd2.css +0 -2
  97. package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
  98. package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
  99. package/dist/devtools/_nuxt/pO9PmtPW.js +0 -6
  100. package/dist/devtools/_nuxt/pages.0Jk10xlb.css +0 -1
  101. package/dist/devtools/_nuxt/renderer-select.CN5IWe60.css +0 -1
  102. package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
  103. package/dist/devtools/debug/index.html +0 -1
  104. package/dist/devtools/docs/index.html +0 -1
  105. package/dist/devtools/index.html +0 -1
  106. package/dist/devtools/templates/index.html +0 -1
  107. package/dist/shared/nuxt-og-image.CJa2KCie.mjs +0 -189
  108. package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
@@ -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
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ require('node:crypto');
3
4
  require('node:fs');
4
5
  require('node:fs/promises');
5
6
  require('node:url');
@@ -10,13 +11,11 @@ require('ohash');
10
11
  require('pathe');
11
12
  require('pkg-types');
12
13
  require('std-env');
13
- const module$1 = require('./shared/nuxt-og-image.CfTPCtaS.cjs');
14
+ const module$1 = require('./shared/nuxt-og-image.yDJBWXBt.cjs');
15
+ require('./shared/nuxt-og-image.RI9DgjgV.cjs');
14
16
  require('nuxtseo-shared/kit');
15
17
  require('../dist/runtime/logger.js');
16
- require('node:crypto');
17
18
  require('oxc-walker');
18
- require('chrome-launcher');
19
- require('nypm');
20
19
  require('nuxtseo-shared/devtools');
21
20
  require('unstorage');
22
21
  require('unstorage/drivers/fs-lite');
@@ -30,9 +29,10 @@ require('ofetch');
30
29
  require('ultrahtml');
31
30
  require('../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js');
32
31
  require('exsolve');
33
- require('./shared/nuxt-og-image.CMYbz66P.cjs');
34
32
  require('magicast');
35
33
  require('magicast/helpers');
34
+ require('nypm');
35
+ require('chrome-launcher');
36
36
 
37
37
 
38
38
 
package/dist/module.d.cts CHANGED
@@ -186,12 +186,14 @@ interface ModuleOptions {
186
186
  * keyed hash signature and the handler rejects requests with missing or
187
187
  * invalid signatures.
188
188
  *
189
- * Must be a stable string across deployments. Generate one with:
189
+ * Leave unset to auto-generate a per-build secret (signing on by default).
190
+ * Set an explicit, stable string for rolling or multi-instance deploys, or
191
+ * `false` to disable signing entirely. Generate one with:
190
192
  * `npx nuxt-og-image generate-secret`
191
193
  *
192
- * Required when `strict` is enabled.
194
+ * Required (explicitly) when `strict` is enabled.
193
195
  */
194
- secret?: string;
196
+ secret?: string | false;
195
197
  /**
196
198
  * Enable strict security mode. When enabled:
197
199
  * - `secret` is required (URL signing)
package/dist/module.d.mts CHANGED
@@ -186,12 +186,14 @@ interface ModuleOptions {
186
186
  * keyed hash signature and the handler rejects requests with missing or
187
187
  * invalid signatures.
188
188
  *
189
- * Must be a stable string across deployments. Generate one with:
189
+ * Leave unset to auto-generate a per-build secret (signing on by default).
190
+ * Set an explicit, stable string for rolling or multi-instance deploys, or
191
+ * `false` to disable signing entirely. Generate one with:
190
192
  * `npx nuxt-og-image generate-secret`
191
193
  *
192
- * Required when `strict` is enabled.
194
+ * Required (explicitly) when `strict` is enabled.
193
195
  */
194
- secret?: string;
196
+ secret?: string | false;
195
197
  /**
196
198
  * Enable strict security mode. When enabled:
197
199
  * - `secret` is required (URL signing)
package/dist/module.d.ts CHANGED
@@ -186,12 +186,14 @@ interface ModuleOptions {
186
186
  * keyed hash signature and the handler rejects requests with missing or
187
187
  * invalid signatures.
188
188
  *
189
- * Must be a stable string across deployments. Generate one with:
189
+ * Leave unset to auto-generate a per-build secret (signing on by default).
190
+ * Set an explicit, stable string for rolling or multi-instance deploys, or
191
+ * `false` to disable signing entirely. Generate one with:
190
192
  * `npx nuxt-og-image generate-secret`
191
193
  *
192
- * Required when `strict` is enabled.
194
+ * Required (explicitly) when `strict` is enabled.
193
195
  */
194
- secret?: string;
196
+ secret?: string | false;
195
197
  /**
196
198
  * Enable strict security mode. When enabled:
197
199
  * - `secret` is required (URL signing)
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.3",
7
+ "version": "6.7.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import 'node:crypto';
1
2
  import 'node:fs';
2
3
  import 'node:fs/promises';
3
4
  import 'node:url';
@@ -8,13 +9,11 @@ import 'ohash';
8
9
  import 'pathe';
9
10
  import 'pkg-types';
10
11
  import 'std-env';
11
- export { m as default } from './shared/nuxt-og-image.DdbTs-xp.mjs';
12
+ export { m as default } from './shared/nuxt-og-image.CPSxZhVO.mjs';
13
+ import './shared/nuxt-og-image.DM30kcDz.mjs';
12
14
  import 'nuxtseo-shared/kit';
13
15
  import '../dist/runtime/logger.js';
14
- import 'node:crypto';
15
16
  import 'oxc-walker';
16
- import 'chrome-launcher';
17
- import 'nypm';
18
17
  import 'nuxtseo-shared/devtools';
19
18
  import 'unstorage';
20
19
  import 'unstorage/drivers/fs-lite';
@@ -28,6 +27,7 @@ import 'ofetch';
28
27
  import 'ultrahtml';
29
28
  import '../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js';
30
29
  import 'exsolve';
31
- import './shared/nuxt-og-image.CJa2KCie.mjs';
32
30
  import 'magicast';
33
31
  import 'magicast/helpers';
32
+ import 'nypm';
33
+ import 'chrome-launcher';
@@ -1,3 +1,3 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { FontConfig } from '../../../../types.js';
3
- export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<ArrayBuffer>>;
3
+ export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<ArrayBufferLike>>;
@@ -1,14 +1,19 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { withBase } from "ufo";
3
+ import { getSiteConfig } from "#site-config/server/composables";
3
4
  import { getCloudflareAssets } from "../../../util/cloudflareAssets.js";
4
5
  import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
5
6
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
6
7
  import { useOgImageRuntimeConfig } from "../../../utils.js";
8
+ import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
7
9
  export async function resolve(event, font) {
8
10
  const path = font.src || font.localPath;
11
+ const runtimeConfig = useOgImageRuntimeConfig();
12
+ const timeout = getFetchTimeout(runtimeConfig);
9
13
  const { app } = useRuntimeConfig();
10
14
  const fullPath = withBase(path, app.baseURL);
11
- const timeout = getFetchTimeout(useOgImageRuntimeConfig());
15
+ if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
16
+ return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
12
17
  const ab = await fetchLocalAsset(event, fullPath, { fetchTimeout: timeout });
13
18
  if (ab)
14
19
  return Buffer.from(ab);
@@ -4,8 +4,11 @@ import { useRuntimeConfig } from "nitropack/runtime";
4
4
  import { join } from "pathe";
5
5
  import { withBase } from "ufo";
6
6
  import { buildDir, rootDir } from "#og-image-virtual/build-dir.mjs";
7
+ import { getSiteConfig } from "#site-config/server/composables";
7
8
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
9
+ import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
8
10
  import { useOgImageRuntimeConfig } from "../../../utils.js";
11
+ import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
9
12
  let fontUrlMapping;
10
13
  async function loadFontUrlMapping() {
11
14
  if (fontUrlMapping)
@@ -16,12 +19,15 @@ async function loadFontUrlMapping() {
16
19
  }
17
20
  export async function resolve(event, font) {
18
21
  const path = font.src || font.localPath;
19
- const timeout = getFetchTimeout(useOgImageRuntimeConfig());
22
+ const runtimeConfig = useOgImageRuntimeConfig();
23
+ const timeout = getFetchTimeout(runtimeConfig);
20
24
  if (font.absolutePath) {
21
25
  const data = await readFile(font.absolutePath).catch(() => null);
22
26
  if (data?.length)
23
27
  return data;
24
28
  }
29
+ if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
30
+ return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
25
31
  if (import.meta.prerender) {
26
32
  if (path.startsWith("/_og-static-fonts/")) {
27
33
  const filename2 = path.slice("/_og-static-fonts/".length);
@@ -72,14 +78,15 @@ export async function resolve(event, font) {
72
78
  if (import.meta.dev) {
73
79
  const reqUrl = getRequestURL(event);
74
80
  const origin = `${reqUrl.protocol}//${reqUrl.host}`;
75
- const url = new URL(withBase(path, app.baseURL), origin).href;
76
- const res = await fetch(url, { signal: AbortSignal.timeout(timeout) }).catch(() => null);
77
- if (res?.ok) {
78
- return Buffer.from(await res.arrayBuffer());
81
+ const target = new URL(withBase(path, app.baseURL), origin);
82
+ const ab = await fetchWithRedirectValidation(target.href, { timeout, trustedHost: target.host }).catch(() => null);
83
+ if (ab) {
84
+ return Buffer.from(ab);
79
85
  }
80
86
  }
81
87
  const fullPath = withBase(path, app.baseURL);
82
- const arrayBuffer = await event.$fetch(fullPath, {
88
+ const fetchArrayBuffer = event.$fetch;
89
+ const arrayBuffer = await fetchArrayBuffer(fullPath, {
83
90
  responseType: "arrayBuffer",
84
91
  timeout
85
92
  });
@@ -0,0 +1,36 @@
1
+ /** True for an inline `data:` font URI (no network — safe to decode directly). */
2
+ export declare function isDataFontUrl(path: string): boolean;
3
+ /**
4
+ * True when a font path carries its own authority (`//host`, `http://host`,
5
+ * `\\host`) or a non-http scheme — i.e. it would not resolve same-origin.
6
+ *
7
+ * Classification resolves the path against a sentinel origin through the WHATWG
8
+ * URL parser, so it sees exactly what `fetch` will: leading C0 controls/spaces,
9
+ * embedded tab/CR/LF, and backslash folding are all normalized first. This
10
+ * closes the bypass class where " //127.0.0.1" or " //169.254.169.254" looks
11
+ * relative to a naive scheme check yet resolves cross-origin (GHSA-q8hw-4fvp-9rwv).
12
+ * Check `isDataFontUrl` first — `data:` is non-http and would report true here.
13
+ */
14
+ export declare function isExternalFontUrl(path: string): boolean;
15
+ /**
16
+ * Resolve an authority-bearing font URL to the href to fetch, but only when it
17
+ * is same-origin with the configured site URL. Returns null otherwise.
18
+ *
19
+ * Runtime external font URLs are unsupported — `@nuxt/fonts` is the only
20
+ * supported way to load custom fonts, and it serves them same-origin. The single
21
+ * allowed exception is the site's own origin, gated on an explicitly configured
22
+ * site URL. Resolving against that origin also collapses canonicalization
23
+ * bypasses (a crafted " //127.0.0.1" resolves cross-origin → rejected).
24
+ */
25
+ export declare function resolveSameOriginFontUrl(path: string, siteUrl: string | undefined): string | null;
26
+ /**
27
+ * Fetch a `data:` or authority-bearing font URL. `data:` is decoded directly
28
+ * (no network). An external URL is fetched only when same-origin with the site
29
+ * URL, and always through the SSRF guard (scheme allowlist, private-network
30
+ * block, per-hop redirect re-validation) so the site's own host can't be used
31
+ * to relay into its internal network via an open redirect.
32
+ *
33
+ * Throws on an unsupported/blocked/unreachable URL so the caller falls back to a
34
+ * bundled font; blocked targets stay indistinguishable from missing fonts.
35
+ */
36
+ export declare function fetchSpecialFontUrl(path: string, siteUrl: string | undefined, timeout: number): Promise<Buffer>;
@@ -0,0 +1,47 @@
1
+ import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
2
+ const SENTINEL_ORIGIN = "http://font-asset.invalid/";
3
+ const SENTINEL_HOST = "font-asset.invalid";
4
+ export function isDataFontUrl(path) {
5
+ return path.trimStart().toLowerCase().startsWith("data:");
6
+ }
7
+ export function isExternalFontUrl(path) {
8
+ let resolved;
9
+ try {
10
+ resolved = new URL(path, SENTINEL_ORIGIN);
11
+ } catch {
12
+ return true;
13
+ }
14
+ return resolved.protocol !== "http:" || resolved.host !== SENTINEL_HOST;
15
+ }
16
+ export function resolveSameOriginFontUrl(path, siteUrl) {
17
+ if (!siteUrl)
18
+ return null;
19
+ let siteOrigin;
20
+ try {
21
+ siteOrigin = new URL(siteUrl).origin;
22
+ } catch {
23
+ return null;
24
+ }
25
+ let target;
26
+ try {
27
+ target = new URL(path, siteOrigin);
28
+ } catch {
29
+ return null;
30
+ }
31
+ return target.origin === siteOrigin ? target.href : null;
32
+ }
33
+ export async function fetchSpecialFontUrl(path, siteUrl, timeout) {
34
+ if (isDataFontUrl(path)) {
35
+ const res = await fetch(path.trimStart(), { signal: AbortSignal.timeout(timeout) }).catch(() => null);
36
+ if (res?.ok)
37
+ return Buffer.from(await res.arrayBuffer());
38
+ throw new Error("[Nuxt OG Image] Invalid data: font URL.");
39
+ }
40
+ const href = resolveSameOriginFontUrl(path, siteUrl);
41
+ if (!href)
42
+ throw new Error("[Nuxt OG Image] External font URLs are not supported. Load custom fonts via @nuxt/fonts.");
43
+ const ab = await fetchWithRedirectValidation(href, { timeout });
44
+ if (!ab)
45
+ throw new Error("[Nuxt OG Image] Font URL blocked or unreachable.");
46
+ return Buffer.from(ab);
47
+ }
@@ -1,3 +1,3 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { FontConfig } from '../../../../types.js';
3
- export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<ArrayBuffer>>;
3
+ export declare function resolve(event: H3Event, font: FontConfig): Promise<Buffer<ArrayBufferLike>>;
@@ -1,19 +1,26 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { withBase } from "ufo";
3
- import { getNitroOrigin } from "#site-config/server/composables";
3
+ import { getNitroOrigin, getSiteConfig } from "#site-config/server/composables";
4
4
  import { getFetchTimeout } from "../../../util/fetchTimeout.js";
5
+ import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
5
6
  import { useOgImageRuntimeConfig } from "../../../utils.js";
7
+ import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
6
8
  export async function resolve(event, font) {
7
9
  const path = font.src || font.localPath;
10
+ const runtimeConfig = useOgImageRuntimeConfig();
11
+ const timeout = getFetchTimeout(runtimeConfig);
8
12
  const { app } = useRuntimeConfig();
9
- const fullPath = withBase(path, app.baseURL);
10
13
  const origin = getNitroOrigin(event);
11
- const timeout = getFetchTimeout(useOgImageRuntimeConfig());
12
- const res = await fetch(new URL(fullPath, origin).href, { signal: AbortSignal.timeout(timeout) }).catch(() => null);
13
- if (res?.ok) {
14
- return Buffer.from(await res.arrayBuffer());
14
+ const fullPath = withBase(path, app.baseURL);
15
+ if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
16
+ return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
17
+ const target = new URL(fullPath, origin);
18
+ const ab = await fetchWithRedirectValidation(target.href, { timeout, trustedHost: target.host }).catch(() => null);
19
+ if (ab) {
20
+ return Buffer.from(ab);
15
21
  }
16
- const arrayBuffer = await event.$fetch(fullPath, {
22
+ const fetchArrayBuffer = event.$fetch;
23
+ const arrayBuffer = await fetchArrayBuffer(fullPath, {
17
24
  responseType: "arrayBuffer",
18
25
  timeout
19
26
  });