@@ -21,11 +21,11 @@ const RE_RENDERER_SUFFIX = /(Satori|Browser|Takumi)$/
2121type OgImagePayload = [ string , OgImageOptionsInternal , string ]
2222
2323/**
24- * Extract title and description from head entries set by useSeoMeta / useHead .
24+ * Extract title and description from head entries set by useSeoMeta.
2525 *
26- * useSeoMeta stores description in `_flatMeta` (flat object keyed by meta name),
27- * while useHead stores it in `input.meta` (array of { name, content } objects ).
28- * Title is hoisted to `entry.input.title` by both APIs .
26+ * Only pulls from useSeoMeta entries (identified by the `_flatMeta` marker)
27+ * so plain ` useHead({ title })` page titles don't leak into og:image URLs (#573 ).
28+ * useSeoMeta hoists title to `entry.input.title` and description to `_flatMeta` .
2929 */
3030function extractHeadSeoProps ( head : VueHeadClient ) : { title ?: string , description ?: string } {
3131 const result : { title ?: string , description ?: string } = { }
@@ -35,33 +35,20 @@ function extractHeadSeoProps(head: VueHeadClient): { title?: string, description
3535 if ( ! input || typeof input !== 'object' )
3636 continue
3737
38- // Title: both useSeoMeta and useHead hoist title to entry.input.title
38+ // `_flatMeta` is only populated by useSeoMeta — use it to distinguish from useHead.
39+ const flatMeta = input . _flatMeta
40+ if ( ! flatMeta || typeof flatMeta !== 'object' )
41+ continue
42+
3943 if ( 'title' in input ) {
4044 const t = toValue ( input . title )
4145 if ( typeof t === 'string' )
4246 result . title = t
4347 }
4448
45- // Description from useSeoMeta: stored in _flatMeta
46- if ( input . _flatMeta && typeof input . _flatMeta === 'object' ) {
47- const d = toValue ( input . _flatMeta . description ) || toValue ( input . _flatMeta . ogDescription )
48- if ( typeof d === 'string' )
49- result . description = d
50- }
51-
52- // Description from useHead({ meta: [...] })
53- if ( Array . isArray ( input . meta ) ) {
54- for ( const meta of input . meta ) {
55- const m = toValue ( meta )
56- if ( ! m || typeof m !== 'object' )
57- continue
58- if ( m . name === 'description' || m . property === 'og:description' ) {
59- const c = toValue ( m . content )
60- if ( typeof c === 'string' )
61- result . description = c
62- }
63- }
64- }
49+ const desc = toValue ( flatMeta . description ) || toValue ( flatMeta . ogDescription )
50+ if ( typeof desc === 'string' )
51+ result . description = desc
6552 }
6653 }
6754 catch ( e ) {
@@ -127,11 +114,20 @@ export function createOgImageMeta(src: string, input: OgImageOptions | OgImagePr
127114 const seo = head ? extractHeadSeoProps ( head ) : undefined
128115 return finalPayload . flatMap ( ( [ _ , options , payloadBasePath ] ) => {
129116 const opts = { ...options , props : { ...options . props } }
130- // Inject title/description from head entries if not explicitly set
117+ // Resolve component with alias/shorthand support so we know which props it declares.
118+ // Skips auto-injection for props the component doesn't accept
119+ // (#573: useHead({ title }) was leaking into URLs of components without a title prop).
120+ const rawComponentName = opts . component || componentNames ?. [ 0 ] ?. pascalName
121+ const resolvedComponentName = rawComponentName ? resolveComponentName ( rawComponentName ) : undefined
122+ const resolvedComponent = resolvedComponentName
123+ ? componentNames ?. find ( ( c : any ) => c . pascalName === resolvedComponentName || c . kebabName === resolvedComponentName )
124+ : undefined
125+ const declaredProps = resolvedComponent ?. propNames
126+ // Inject title/description from head entries if not explicitly set AND the component declares them
131127 if ( seo ) {
132- if ( seo . title && typeof opts . props . title === 'undefined' )
128+ if ( seo . title && typeof opts . props . title === 'undefined' && ( ! declaredProps || declaredProps . includes ( 'title' ) ) )
133129 opts . props . title = seo . title
134- if ( seo . description && typeof opts . props . description === 'undefined' )
130+ if ( seo . description && typeof opts . props . description === 'undefined' && ( ! declaredProps || declaredProps . includes ( 'description' ) ) )
135131 opts . props . description = seo . description
136132 }
137133 // Inline getOgImagePath logic: useRuntimeConfig() is unavailable in lazy callbacks
@@ -141,6 +137,8 @@ export function createOgImageMeta(src: string, input: OgImageOptions | OgImagePr
141137 // setups where pages are prerendered but OG images are served dynamically.
142138 const isStatic = import . meta. prerender && ! ( ogImageConfig . security ?. secret && ogImageConfig . security ?. strict )
143139 const urlOpts : Record < string , any > = { ...opts , _path : payloadBasePath }
140+ // Use a strict (non-aliased) lookup for hash — matches main behavior so URLs
141+ // stay stable for shorthand component names like `Default` or `NuxtSeo.satori`.
144142 const componentName = opts . component || componentNames ?. [ 0 ] ?. pascalName
145143 const component = componentNames ?. find ( ( c : any ) => c . pascalName === componentName || c . kebabName === componentName )
146144 if ( component ?. hash )
@@ -190,11 +188,18 @@ export function createOgImageMeta(src: string, input: OgImageOptions | OgImagePr
190188 const seo = head ? extractHeadSeoProps ( head ) : undefined
191189 const devtoolsPayload = ( ssrContext . _ogImagePayloads || [ ] ) . map ( ( [ key , options ] ) => {
192190 const payload = resolveUnrefHeadInput ( options ) as any
191+ // Resolve component to check declared props before auto-injecting title/description
192+ const rawComponentName = payload . component || componentNames ?. [ 0 ] ?. pascalName
193+ const resolvedComponentName = rawComponentName ? resolveComponentName ( rawComponentName ) : undefined
194+ const component = resolvedComponentName
195+ ? componentNames ?. find ( ( c : any ) => c . pascalName === resolvedComponentName || c . kebabName === resolvedComponentName )
196+ : undefined
197+ const declaredProps = component ?. propNames
193198 // Use %s template param for title so unhead resolves it with titleTemplate
194- if ( payload . props && typeof payload . props . title === 'undefined' )
199+ if ( payload . props && typeof payload . props . title === 'undefined' && ( ! declaredProps || declaredProps . includes ( 'title' ) ) )
195200 payload . props . title = '%s'
196201 // Inject description from head entries for devtools/prerender cache
197- if ( seo ?. description && payload . props && typeof payload . props . description === 'undefined' )
202+ if ( seo ?. description && payload . props && typeof payload . props . description === 'undefined' && ( ! declaredProps || declaredProps . includes ( 'description' ) ) )
198203 payload . props . description = seo . description
199204 if ( typeof payload . component === 'string' ) {
200205 payload . component = resolveComponentName ( payload . component )
0 commit comments