1- // eslint-disable-next-line eslint-comments/disable-enable-pair
2- /* eslint-disable @typescript-eslint/no-explicit-any */
3- import { Maybe } from './storefront-api-types.js' ;
4- import { WithContext } from 'schema-dts' ;
5-
6- export interface BaseSeo {
7- description ?: any ;
8- media ?: any ;
9- title ?: any ;
10- titleTemplate ?: any ;
11- url ?: any ;
12- handle ?: any ;
13- ldJson ?: any ;
14- alternates ?: any [ ] ;
15- }
1+ import type { Maybe } from './storefront-api-types.js' ;
2+ import type { WithContext , Thing } from 'schema-dts' ;
3+ import type { ComponentPropsWithoutRef , HTMLAttributes } from 'react' ;
164
175export interface Seo {
186 /**
@@ -202,9 +190,9 @@ export type SeoMedia = {
202190
203191export type TagKey = 'title' | 'base' | 'meta' | 'link' | 'script' ;
204192
205- export interface HeadTag {
193+ export interface CustomHeadTagObject {
206194 tag : TagKey ;
207- props : Record < string , any > ;
195+ props : Record < string , unknown > ;
208196 children ?: string ;
209197 key : string ;
210198}
@@ -223,39 +211,43 @@ export type SchemaType =
223211 * pairs well with the SEO component in `@shopify/hydrogen` when building a Hydrogen Remix app, but can be used on its
224212 * own if you want to generate the tags yourself.
225213 */
226- export function generateSeoTags < T extends BaseSeo = Seo > ( input : T ) {
227- const output : HeadTag [ ] = [ ] ;
228- let ldJson : WithContext < any > = {
214+ export function generateSeoTags < T extends Seo = Seo > (
215+ seoInput : T
216+ ) : CustomHeadTagObject [ ] {
217+ const output : CustomHeadTagObject [ ] = [ ] ;
218+
219+ // https://github.com/google/schema-dts/issues/98
220+ let ldJson : WithContext < Exclude < Thing , string > > = {
229221 '@context' : 'https://schema.org' ,
230222 '@type' : 'Thing' ,
231223 } ;
232224
233- for ( const tag of Object . keys ( input ) ) {
234- const values = Array . isArray ( input [ tag as keyof T ] )
235- ? ( input [ tag as keyof T ] as [ keyof T ] [ ] )
236- : [ input [ tag as keyof T ] ] ;
225+ for ( const seoKey of Object . keys ( seoInput ) as ( keyof T ) [ ] ) {
226+ const values = Array . isArray ( seoInput [ seoKey ] )
227+ ? ( seoInput [ seoKey ] as T [ keyof T ] [ ] )
228+ : [ seoInput [ seoKey ] ] ;
237229
238230 const tags = values . map ( ( value ) => {
239- const tagResults : any [ ] = [ ] ;
231+ const tagResults : CustomHeadTagObject [ ] = [ ] ;
240232
241233 if ( ! value ) {
242234 return tagResults ;
243235 }
244236
245- switch ( tag ) {
246- case 'title' :
247- // eslint-disable-next-line no-case-declarations
248- const title = renderTitle ( input . titleTemplate , value as string ) ;
237+ switch ( seoKey ) {
238+ case 'title' : {
239+ const title = renderTitle ( seoInput ?. titleTemplate , value ) ;
249240
250241 tagResults . push (
251- generateTag ( 'title' , title ) ,
242+ generateTag ( 'title' , { title} ) ,
252243 generateTag ( 'meta' , { property : 'og:title' , content : title } ) ,
253244 generateTag ( 'meta' , { name : 'twitter:title' , content : title } )
254245 ) ;
255246
256247 ldJson . name = title ;
257248
258249 break ;
250+ }
259251
260252 case 'description' :
261253 tagResults . push (
@@ -291,9 +283,8 @@ export function generateSeoTags<T extends BaseSeo = Seo>(input: T) {
291283 ldJson = { ...ldJson , ...value } ;
292284 break ;
293285
294- case 'media' :
295- // eslint-disable-next-line no-case-declarations
296- const values : any = Array . isArray ( value ) ? value : [ value ] ;
286+ case 'media' : {
287+ const values = Array . isArray ( value ) ? value : [ value ] ;
297288
298289 for ( const media of values ) {
299290 if ( typeof media === 'string' ) {
@@ -337,19 +328,16 @@ export function generateSeoTags<T extends BaseSeo = Seo>(input: T) {
337328 }
338329 }
339330 break ;
331+ }
340332
341- case 'alternates' :
342- // eslint-disable-next-line no-case-declarations
333+ case 'alternates' : {
343334 const alternates = Array . isArray ( value ) ? value : [ value ] ;
344335
345336 for ( const alternate of alternates ) {
346337 const {
347- // @ts -expect-error untyped
348338 language,
349- // @ts -expect-error untyped
350339 media,
351340 url,
352- // @ts -expect-error untyped
353341 default : defaultLang ,
354342 } = alternate as Seo [ 'alternates' ] [ 0 ] ;
355343
@@ -368,6 +356,7 @@ export function generateSeoTags<T extends BaseSeo = Seo>(input: T) {
368356 }
369357
370358 break ;
359+ }
371360 }
372361
373362 return tagResults ;
@@ -402,24 +391,56 @@ export function generateSeoTags<T extends BaseSeo = Seo>(input: T) {
402391 . flat ( ) ;
403392}
404393
405- function generateTag < T extends HeadTag > (
406- tagName : T [ 'tag' ] ,
407- input : any ,
394+ type MetaTagProps =
395+ | ComponentPropsWithoutRef < 'title' >
396+ | ComponentPropsWithoutRef < 'base' >
397+ | ComponentPropsWithoutRef < 'meta' >
398+ | ComponentPropsWithoutRef < 'link' >
399+ | ComponentPropsWithoutRef < 'script' > ;
400+
401+ function generateTag (
402+ tagName : 'title' ,
403+ input : ComponentPropsWithoutRef < 'title' > ,
404+ group ?: string
405+ ) : CustomHeadTagObject ;
406+ function generateTag (
407+ tagName : 'base' ,
408+ input : ComponentPropsWithoutRef < 'base' > ,
409+ group ?: string
410+ ) : CustomHeadTagObject ;
411+ function generateTag (
412+ tagName : 'meta' ,
413+ input : ComponentPropsWithoutRef < 'meta' > ,
414+ group ?: string
415+ ) : CustomHeadTagObject ;
416+ function generateTag (
417+ tagName : 'link' ,
418+ input : ComponentPropsWithoutRef < 'link' > ,
419+ group ?: string
420+ ) : CustomHeadTagObject ;
421+ function generateTag (
422+ tagName : 'script' ,
423+ input : ComponentPropsWithoutRef < 'script' > ,
424+ group ?: string
425+ ) : CustomHeadTagObject ;
426+ function generateTag (
427+ tagName : TagKey ,
428+ input : MetaTagProps ,
408429 group ?: string
409- ) : T | T [ ] {
410- const tag = { tag : tagName , props : { } } as T ;
430+ ) : CustomHeadTagObject {
431+ const tag : CustomHeadTagObject = { tag : tagName , props : { } , key : '' } ;
411432
412433 // title tags don't have props so move to children
413434 if ( tagName === 'title' ) {
414- tag . children = input ;
435+ tag . children = JSON . stringify ( input ) ;
415436 tag . key = generateKey ( tag ) ;
416437
417438 return tag ;
418439 }
419440
420441 // also move the input children to children and delete it
421442 if ( tagName === 'script' ) {
422- tag . children = input . children ;
443+ tag . children = JSON . stringify ( input . children ) ;
423444
424445 delete input . children ;
425446 }
@@ -437,7 +458,7 @@ function generateTag<T extends HeadTag>(
437458 return tag ;
438459}
439460
440- function generateKey ( tag : HeadTag , group ?: string ) {
461+ function generateKey ( tag : CustomHeadTagObject , group ?: string ) {
441462 const { tag : tagName , props} = tag ;
442463
443464 if ( tagName === 'title' ) {
@@ -469,8 +490,12 @@ function generateKey(tag: HeadTag, group?: string) {
469490 return `${ tagName } -${ props . type } ` ;
470491}
471492
472- function renderTitle < T extends HeadTag [ 'children' ] > (
473- template ?: string | ( ( title ?: string ) => string | undefined ) ,
493+ function renderTitle < T extends CustomHeadTagObject [ 'children' ] > (
494+ template ?:
495+ | string
496+ | ( ( title ?: string ) => string | undefined )
497+ | undefined
498+ | null ,
474499 title ?: T
475500) : string | undefined {
476501 if ( ! template ) {
0 commit comments