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.
- package/README.md +2 -0
- package/dist/chunks/tw4.cjs +7 -7
- package/dist/chunks/tw4.mjs +7 -7
- package/dist/chunks/uno.cjs +6 -6
- package/dist/chunks/uno.mjs +6 -6
- package/dist/cli.cjs +12 -5
- package/dist/cli.mjs +10 -3
- package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
- package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
- package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
- package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
- package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
- package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
- package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
- package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
- package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
- package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
- package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
- package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
- package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
- package/dist/devtools/lib/og-image/keys.ts +8 -0
- package/dist/devtools/lib/og-image/og-image.ts +536 -0
- package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
- package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
- package/dist/devtools/lib/og-image/rpc.ts +73 -0
- package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
- package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
- package/dist/devtools/lib/og-image/shared.ts +30 -0
- package/dist/devtools/lib/og-image/templates.ts +9 -0
- package/dist/devtools/lib/og-image/types.ts +38 -0
- package/dist/devtools/lib/og-image/util/logic.ts +21 -0
- package/dist/devtools/nuxt.config.ts +6 -0
- package/dist/devtools/pages/og-image/debug.vue +32 -0
- package/dist/devtools/pages/og-image/docs.vue +3 -0
- package/dist/devtools/pages/og-image/index.vue +1682 -0
- package/dist/devtools/pages/og-image/templates.vue +150 -0
- package/dist/devtools/pages/og-image.vue +184 -0
- package/dist/module.cjs +5 -5
- package/dist/module.d.cts +5 -3
- package/dist/module.d.mts +5 -3
- package/dist/module.d.ts +5 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -5
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +6 -1
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +13 -6
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +36 -0
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +47 -0
- package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +14 -7
- package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +7 -1
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
- package/dist/runtime/server/og-image/fonts.d.ts +4 -0
- package/dist/runtime/server/og-image/fonts.js +21 -5
- package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
- package/dist/runtime/server/og-image/takumi/renderer.js +61 -19
- package/dist/runtime/server/util/ssrf.d.ts +10 -0
- package/dist/runtime/server/util/ssrf.js +9 -1
- package/dist/runtime/types.d.ts +13 -0
- package/dist/shared/{nuxt-og-image.DdbTs-xp.mjs → nuxt-og-image.CPSxZhVO.mjs} +122 -496
- package/dist/shared/nuxt-og-image.DM30kcDz.mjs +660 -0
- package/dist/shared/nuxt-og-image.RI9DgjgV.cjs +689 -0
- package/dist/shared/{nuxt-og-image.CfTPCtaS.cjs → nuxt-og-image.yDJBWXBt.cjs} +181 -555
- package/package.json +48 -45
- package/dist/devtools/200.html +0 -1
- package/dist/devtools/404.html +0 -1
- package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
- package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
- package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
- package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
- package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
- package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
- package/dist/devtools/_nuxt/BZ_CG9ll.js +0 -30
- package/dist/devtools/_nuxt/BdMalyEU.js +0 -4
- package/dist/devtools/_nuxt/BhkGaWEe.js +0 -3
- package/dist/devtools/_nuxt/Bm4Q-nTW.js +0 -3
- package/dist/devtools/_nuxt/BsRKQH5X.js +0 -1
- package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
- package/dist/devtools/_nuxt/CBcJg2GG.js +0 -2
- package/dist/devtools/_nuxt/CDaXKRRu.js +0 -1
- package/dist/devtools/_nuxt/CM-t-6ZT.js +0 -1
- package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
- package/dist/devtools/_nuxt/CXAM78_A.js +0 -1
- package/dist/devtools/_nuxt/CYDI7SLa.js +0 -1
- package/dist/devtools/_nuxt/CkbDfutK.js +0 -152
- package/dist/devtools/_nuxt/Co8Tk_C2.js +0 -1
- package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
- package/dist/devtools/_nuxt/D49I50CU.js +0 -1
- package/dist/devtools/_nuxt/DevtoolsSection.DkT2bJJh.css +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.zV_SDojn.css +0 -1
- package/dist/devtools/_nuxt/HWyD49jH.js +0 -1
- package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
- package/dist/devtools/_nuxt/PONEy9N-.js +0 -1
- package/dist/devtools/_nuxt/builds/latest.json +0 -1
- package/dist/devtools/_nuxt/builds/meta/0f3dbc15-8308-4956-81b9-5507cac7018c.json +0 -1
- package/dist/devtools/_nuxt/entry.D7u2asd2.css +0 -2
- package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
- package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
- package/dist/devtools/_nuxt/pO9PmtPW.js +0 -6
- package/dist/devtools/_nuxt/pages.0Jk10xlb.css +0 -1
- package/dist/devtools/_nuxt/renderer-select.CN5IWe60.css +0 -1
- package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
- package/dist/devtools/debug/index.html +0 -1
- package/dist/devtools/docs/index.html +0 -1
- package/dist/devtools/index.html +0 -1
- package/dist/devtools/templates/index.html +0 -1
- package/dist/shared/nuxt-og-image.CJa2KCie.mjs +0 -189
- 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…
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
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.
|
|
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<
|
|
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
|
-
|
|
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
|
|
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
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
return Buffer.from(
|
|
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
|
|
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<
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
22
|
+
const fetchArrayBuffer = event.$fetch;
|
|
23
|
+
const arrayBuffer = await fetchArrayBuffer(fullPath, {
|
|
17
24
|
responseType: "arrayBuffer",
|
|
18
25
|
timeout
|
|
19
26
|
});
|