• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ryanhefner / next-meta / ec45e0e7-945d-4321-8c22-223ed21eb739

20 Dec 2025 06:20PM UTC coverage: 87.898% (-11.0%) from 98.853%
ec45e0e7-945d-4321-8c22-223ed21eb739

Pull #5

circleci

ryanhefner
Update Schema component to properly support all Schema.org types
Pull Request #5: v0.4.0

146 of 168 branches covered (86.9%)

Branch coverage included in aggregate %.

34 of 50 new or added lines in 6 files covered. (68.0%)

130 of 146 relevant lines covered (89.04%)

53.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.21
/src/renderMeta.tsx
1
import React from 'react'
2
import merge from 'lodash/merge'
3
import type { SiteMetaProps, Image } from './types'
4

5
export const getAbsoluteUrl = (
6✔
6
  url: string | undefined,
7
  baseUrl?: string,
8
): string | undefined => {
9
  if (baseUrl && url && url.indexOf('http') === -1) {
819✔
10
    return `${baseUrl}${url}`
19✔
11
  }
12

13
  return url
800✔
14
}
15

16
const DEFAULTS = {
6✔
17
  siteNameDelimiter: '|',
18
}
19

20
export const renderMeta = (
6✔
21
  props: SiteMetaProps = {},
163✔
22
  context: SiteMetaProps = {},
163✔
23
): React.ReactNode[] => {
24
  const {
25
    audioUrl,
26
    audioType,
27
    baseUrl,
28
    canonical,
29
    debug,
30
    description,
31
    determiner,
32
    image,
33
    images,
34
    imageUrl,
35
    imageAlt,
36
    imageWidth,
37
    imageHeight,
38
    locale,
39
    localeAlternates,
40
    siteName,
41
    siteNameDelimiter,
42
    title,
43
    twitter,
44
    twitterCard,
45
    twitterCreator,
46
    twitterSite,
47
    type,
48
    url,
49
    videoUrl,
50
    videoType,
51
  } = merge({}, DEFAULTS, context, props)
163✔
52

53
  const absoluteAudioUrl = getAbsoluteUrl(audioUrl, baseUrl)
163✔
54
  const absoluteImageUrl = getAbsoluteUrl(imageUrl ?? image?.url, baseUrl)
163✔
55
  const absoluteVideoUrl = getAbsoluteUrl(videoUrl, baseUrl)
163✔
56
  const absoluteUrl = getAbsoluteUrl(url, baseUrl)
163✔
57
  const absoluteCanonicalUrl = getAbsoluteUrl(canonical, baseUrl)
163✔
58

59
  const tagsToRender: React.ReactNode[] = []
163✔
60

61
  // canonical
62
  if (absoluteCanonicalUrl) {
163✔
63
    tagsToRender.push(
6✔
64
      <link key="canonical" rel="canonical" href={absoluteCanonicalUrl} />,
65
    )
66
  }
67

68
  // title
69
  if (title) {
163✔
70
    tagsToRender.push(
21✔
71
      <title key="meta-title">{`${title}${
72
        siteName ? ` ${siteNameDelimiter} ${siteName}` : ''
21✔
73
      }`}</title>,
74
      <meta key="meta-og-title" property="og:title" content={title} />,
75
      <meta key="meta-twitter-title" name="twitter:title" content={title} />,
76
    )
77
  }
78

79
  // description
80
  if (description) {
163✔
81
    tagsToRender.push(
13✔
82
      <meta key="meta-description" name="description" content={description} />,
83
      <meta
84
        key="meta-og-description"
85
        property="og:description"
86
        content={description}
87
      />,
88
      <meta
89
        key="meta-twitter-description"
90
        name="twitter:description"
91
        content={description}
92
      />,
93
    )
94
  }
95

96
  // locale
97
  if (locale) {
163✔
98
    tagsToRender.push(
3✔
99
      <meta key="meta-og-locale" property="og:locale" content={locale} />,
100
    )
101
  }
102

103
  // locale alternates
104
  if (localeAlternates && localeAlternates.length) {
163✔
105
    tagsToRender.push(
3✔
106
      ...localeAlternates.map((localeAlternate) => (
107
        <meta
6✔
108
          key={`meta-og-locale-alternate-${localeAlternate}`}
109
          property="og:locale:alternate"
110
          content={localeAlternate}
111
        />
112
      )),
113
    )
114
  }
115

116
  // Collect all images to render
117
  // Priority: imageUrl (legacy) > images array > single image
118
  const allImages: Image[] = []
163✔
119

120
  // Legacy imageUrl takes priority for backward compatibility
121
  const hasLegacyImageUrl = absoluteImageUrl
163✔
122

123
  if (!hasLegacyImageUrl) {
163✔
124
    // If images array is provided, use it; otherwise fall back to single image
125
    if (images && images.length > 0) {
145!
NEW
126
      allImages.push(...images)
×
127
    } else if (image) {
145!
NEW
128
      allImages.push(image)
×
129
    }
130
  }
131

132
  // Render images array (Open Graph supports multiple images)
133
  if (allImages.length > 0) {
163!
NEW
134
    allImages.forEach((img, index) => {
×
NEW
135
      const absoluteImgUrl = getAbsoluteUrl(img.url, baseUrl)
×
NEW
136
      if (absoluteImgUrl) {
×
NEW
137
        tagsToRender.push(
×
138
          <meta
139
            key={`meta-og-image-${index}`}
140
            property="og:image"
141
            content={absoluteImgUrl}
142
          />,
143
        )
144

145
        // For Twitter, only use the first image
NEW
146
        if (index === 0) {
×
NEW
147
          tagsToRender.push(
×
148
            <meta
149
              key="meta-twitter-image"
150
              name="twitter:image"
151
              content={absoluteImgUrl}
152
            />,
153
          )
154
        }
155

156
        // imageAlt
NEW
157
        if (img.alt) {
×
NEW
158
          tagsToRender.push(
×
159
            <meta
160
              key={`meta-og-image-alt-${index}`}
161
              property="og:image:alt"
162
              content={img.alt}
163
            />,
164
          )
165
          // For Twitter, only use the first image alt
NEW
166
          if (index === 0) {
×
NEW
167
            tagsToRender.push(
×
168
              <meta
169
                key="meta-twitter-image-alt"
170
                name="twitter:image:alt"
171
                content={img.alt}
172
              />,
173
            )
174
          }
175
        }
176

177
        // imageWidth
NEW
178
        if (img.width !== undefined) {
×
NEW
179
          tagsToRender.push(
×
180
            <meta
181
              key={`meta-og-image-width-${index}`}
182
              property="og:image:width"
183
              content={String(img.width)}
184
            />,
185
          )
186
        }
187

188
        // imageHeight
NEW
189
        if (img.height !== undefined) {
×
NEW
190
          tagsToRender.push(
×
191
            <meta
192
              key={`meta-og-image-height-${index}`}
193
              property="og:image:height"
194
              content={String(img.height)}
195
            />,
196
          )
197
        }
198
      }
199
    })
200
  }
201

202
  // Legacy image handling (for backward compatibility with imageUrl prop)
203
  if (hasLegacyImageUrl) {
163✔
204
    tagsToRender.push(
18✔
205
      <meta
206
        key="meta-og-image"
207
        property="og:image"
208
        content={absoluteImageUrl}
209
      />,
210
      <meta
211
        key="meta-twitter-image"
212
        name="twitter:image"
213
        content={absoluteImageUrl}
214
      />,
215
    )
216

217
    // imageAlt
218
    if (imageAlt || image?.alt) {
18✔
219
      tagsToRender.push(
4✔
220
        <meta
221
          key="meta-og-image-alt"
222
          property="og:image:alt"
223
          content={imageAlt ?? image?.alt}
5✔
224
        />,
225
        <meta
226
          key="meta-twitter-image-alt"
227
          name="twitter:image:alt"
228
          content={imageAlt ?? image?.alt}
5✔
229
        />,
230
      )
231
    }
232

233
    // imageWidth
234
    if (imageWidth !== undefined || image?.width) {
18✔
235
      tagsToRender.push(
6✔
236
        <meta
237
          key="meta-og-image-width"
238
          property="og:image:width"
239
          content={String(imageWidth ?? image?.width)}
7✔
240
        />,
241
      )
242
    }
243

244
    // imageHeight
245
    if (imageHeight !== undefined || image?.height) {
18✔
246
      tagsToRender.push(
6✔
247
        <meta
248
          key="meta-og-image-height"
249
          property="og:image:height"
250
          content={String(imageHeight ?? image?.height)}
7✔
251
        />,
252
      )
253
    }
254
  }
255

256
  // determiner
257
  if (determiner) {
163✔
258
    tagsToRender.push(
3✔
259
      <meta
260
        key="meta-og-determiner"
261
        property="og:determiner"
262
        content={determiner}
263
      />,
264
    )
265
  }
266

267
  // siteName
268
  if (siteName) {
163✔
269
    tagsToRender.push(
11✔
270
      <meta
271
        key="meta-og-site-name"
272
        property="og:site_name"
273
        content={siteName}
274
      />,
275
    )
276
  }
277

278
  // twitterCard
279
  if (twitterCard) {
163✔
280
    tagsToRender.push(
3✔
281
      <meta
282
        key="meta-twitter-card"
283
        name="twitter:card"
284
        content={twitterCard}
285
      />,
286
    )
287
  }
288

289
  // Twitter
290
  if (twitter) {
163✔
291
    // Twitter - Card
292
    if (twitter.card) {
57✔
293
      tagsToRender.push(
7✔
294
        <meta
295
          key="meta-twitter-card"
296
          name="twitter:card"
297
          content={twitter.card}
298
        />,
299
      )
300
    }
301

302
    // Twitter - Image
303
    if (twitter.image) {
57✔
304
      tagsToRender.push(
1✔
305
        <meta
306
          key="meta-twitter-image"
307
          name="twitter:image"
308
          content={twitter.image.url || ''}
1!
309
        />,
310
      )
311

312
      // Twitter - Image: Alt
313
      if (twitter.image.alt) {
1!
314
        tagsToRender.push(
1✔
315
          <meta
316
            key="meta-twitter-image-alt"
317
            name="twitter:image:alt"
318
            content={twitter.image.alt}
319
          />,
320
        )
321
      }
322

323
      // Twitter - Image: Width
324
      if (twitter.image.width) {
1!
325
        tagsToRender.push(
1✔
326
          <meta
327
            key="meta-twitter-image-width"
328
            name="twitter:image:width"
329
            content={String(twitter.image.width)}
330
          />,
331
        )
332
      }
333

334
      // Twitter - Image: Height
335
      if (twitter.image.height) {
1!
336
        tagsToRender.push(
1✔
337
          <meta
338
            key="meta-twitter-image-height"
339
            name="twitter:image:height"
340
            content={String(twitter.image.height)}
341
          />,
342
        )
343
      }
344
    }
345

346
    // Twitter - Site
347
    if (twitter.site) {
57✔
348
      tagsToRender.push(
3✔
349
        <meta
350
          key="meta-twitter-site"
351
          name="twitter:site"
352
          content={twitter.site}
353
        />,
354
      )
355
    }
356

357
    // Twitter - Creator
358
    if (twitter.creator) {
57✔
359
      tagsToRender.push(
5✔
360
        <meta
361
          key="meta-twitter-creator"
362
          name="twitter:creator"
363
          content={twitter.creator}
364
        />,
365
      )
366
    }
367

368
    // Twitter - App
369
    if (twitter.app) {
57✔
370
      // Twitter - App: Country
371
      if (twitter.app.country) {
31✔
372
        tagsToRender.push(
3✔
373
          <meta
374
            key="meta-twitter-app-country"
375
            name="twitter:app:country"
376
            content={twitter.app.country}
377
          />,
378
        )
379
      }
380

381
      // Twitter - App: Google Play
382
      if (twitter.app.googlePlay) {
31✔
383
        // Twitter - App: Name
384
        if (twitter.app.googlePlay.name || twitter.app.name) {
10✔
385
          tagsToRender.push(
6✔
386
            <meta
387
              key="meta-twitter-app-name-googleplay"
388
              name="twitter:app:name:googleplay"
389
              content={twitter.app.googlePlay.name ?? twitter.app.name}
9✔
390
            />,
391
          )
392
        }
393

394
        // Twitter - App: Id
395
        if (twitter.app.googlePlay.id) {
10✔
396
          tagsToRender.push(
6✔
397
            <meta
398
              key="meta-twitter-app-id-googleplay"
399
              name="twitter:app:id:googleplay"
400
              content={twitter.app.googlePlay.id}
401
            />,
402
          )
403
        }
404

405
        // Twitter - App: Url
406
        if (twitter.app.googlePlay.url) {
10✔
407
          tagsToRender.push(
4✔
408
            <meta
409
              key="meta-twitter-app-url-googleplay"
410
              name="twitter:app:url:googleplay"
411
              content={twitter.app.googlePlay.url}
412
            />,
413
          )
414
        }
415
      }
416

417
      // Twitter - App: iPad
418
      if (twitter.app.iPad) {
31✔
419
        // Twitter - App: Name
420
        if (twitter.app.iPad.name || twitter.app.name) {
9✔
421
          tagsToRender.push(
5✔
422
            <meta
423
              key="meta-twitter-app-name-ipad"
424
              name="twitter:app:name:ipad"
425
              content={twitter.app.iPad.name ?? twitter.app.name}
7✔
426
            />,
427
          )
428
        }
429

430
        // Twitter - App: Id
431
        if (twitter.app.iPad.id) {
9✔
432
          tagsToRender.push(
5✔
433
            <meta
434
              key="meta-twitter-app-id-ipad"
435
              name="twitter:app:id:ipad"
436
              content={twitter.app.iPad.id}
437
            />,
438
          )
439
        }
440

441
        // Twitter - App: Url
442
        if (twitter.app.iPad.url) {
9✔
443
          tagsToRender.push(
3✔
444
            <meta
445
              key="meta-twitter-app-url-ipad"
446
              name="twitter:app:url:ipad"
447
              content={twitter.app.iPad.url}
448
            />,
449
          )
450
        }
451
      }
452

453
      // Twitter - App: iPhone
454
      if (twitter.app.iPhone) {
31✔
455
        // Twitter - App: Name
456
        if (twitter.app.iPhone.name || twitter.app.name) {
9✔
457
          tagsToRender.push(
5✔
458
            <meta
459
              key="meta-twitter-app-name-iphone"
460
              name="twitter:app:name:iphone"
461
              content={twitter.app.iPhone.name ?? twitter.app.name}
7✔
462
            />,
463
          )
464
        }
465

466
        // Twitter - App: Id
467
        if (twitter.app.iPhone.id) {
9✔
468
          tagsToRender.push(
5✔
469
            <meta
470
              key="meta-twitter-app-id-iphone"
471
              name="twitter:app:id:iphone"
472
              content={twitter.app.iPhone.id}
473
            />,
474
          )
475
        }
476

477
        // Twitter - App: Url
478
        if (twitter.app.iPhone.url) {
9✔
479
          tagsToRender.push(
3✔
480
            <meta
481
              key="meta-twitter-app-url-iphone"
482
              name="twitter:app:url:iphone"
483
              content={twitter.app.iPhone.url}
484
            />,
485
          )
486
        }
487
      }
488
    }
489

490
    // Twitter - Player
491
    if (twitter.player) {
57✔
492
      // Twitter - Player
493
      if (twitter.player.url) {
14!
494
        tagsToRender.push(
14✔
495
          <meta
496
            key="meta-twitter-player"
497
            name="twitter:player"
498
            content={twitter.player.url}
499
          />,
500
        )
501
      }
502

503
      // Twitter - Player: Width
504
      if (twitter.player.width) {
14✔
505
        tagsToRender.push(
5✔
506
          <meta
507
            key="meta-twitter-player-width"
508
            name="twitter:player:width"
509
            content={twitter.player.width}
510
          />,
511
        )
512
      }
513

514
      // Twitter - Player: Height
515
      if (twitter.player.height) {
14✔
516
        tagsToRender.push(
5✔
517
          <meta
518
            key="meta-twitter-player-height"
519
            name="twitter:player:height"
520
            content={twitter.player.height}
521
          />,
522
        )
523
      }
524

525
      // Twitter - Player: Stream
526
      if (twitter.player.stream) {
14✔
527
        // Twitter - Player: Stream: Url
528
        if (twitter.player.stream.url) {
6!
529
          tagsToRender.push(
6✔
530
            <meta
531
              key="meta-twitter-player-stream"
532
              name="twitter:player:stream"
533
              content={twitter.player.stream.url}
534
            />,
535
          )
536
        }
537

538
        // Twitter - Player: Stream: Content Type
539
        if (twitter.player.stream.contentType) {
6✔
540
          tagsToRender.push(
4✔
541
            <meta
542
              key="meta-twitter-player-stream-content-type"
543
              name="twitter:player:stream:content_type"
544
              content={twitter.player.stream.contentType}
545
            />,
546
          )
547
        }
548
      }
549
    }
550
  }
551

552
  // twitterCreator
553
  if (twitterCreator) {
163✔
554
    tagsToRender.push(
3✔
555
      <meta
556
        key="meta-twitter-creator"
557
        name="twitter:creator"
558
        content={twitterCreator}
559
      />,
560
    )
561
  }
562

563
  // twitterSite
564
  if (twitterSite) {
163✔
565
    tagsToRender.push(
3✔
566
      <meta
567
        key="meta-twitter-site"
568
        name="twitter:site"
569
        content={twitterSite}
570
      />,
571
    )
572
  }
573

574
  // type
575
  if (type) {
163✔
576
    tagsToRender.push(
3✔
577
      <meta key="meta-og-type" property="og:type" content={type} />,
578
    )
579
  }
580

581
  // url
582
  if (absoluteUrl) {
163✔
583
    tagsToRender.push(
10✔
584
      <meta key="meta-og-url" property="og:url" content={absoluteUrl} />,
585
    )
586
  }
587

588
  // audio
589
  if (absoluteAudioUrl) {
163✔
590
    tagsToRender.push(
13✔
591
      <meta
592
        key="meta-og-audio"
593
        property="og:audio"
594
        content={absoluteAudioUrl}
595
      />,
596
    )
597

598
    // audio - secure_url
599
    if (absoluteAudioUrl.startsWith('https://')) {
13✔
600
      tagsToRender.push(
7✔
601
        <meta
602
          key="meta-og-audio-secure-url"
603
          property="og:audio:secure_url"
604
          content={absoluteAudioUrl}
605
        />,
606
      )
607
    }
608

609
    // audioType
610
    if (audioType) {
13✔
611
      tagsToRender.push(
4✔
612
        <meta
613
          key="meta-og-audio-type"
614
          property="og:audio:type"
615
          content={audioType}
616
        />,
617
      )
618
    }
619
  }
620

621
  // video
622
  if (absoluteVideoUrl) {
163✔
623
    tagsToRender.push(
12✔
624
      <meta
625
        key="meta-og-video"
626
        property="og:video"
627
        content={absoluteVideoUrl}
628
      />,
629
    )
630

631
    // video - secure_url
632
    if (absoluteVideoUrl.startsWith('https://')) {
12✔
633
      tagsToRender.push(
6✔
634
        <meta
635
          key="meta-og-video-secure-url"
636
          property="og:video:secure_url"
637
          content={absoluteVideoUrl}
638
        />,
639
      )
640
    }
641

642
    // videoType
643
    if (videoType) {
12✔
644
      tagsToRender.push(
3✔
645
        <meta
646
          key="meta-og-video-type"
647
          property="og:video:type"
648
          content={videoType}
649
        />,
650
      )
651
    }
652
  }
653

654
  return tagsToRender
163✔
655
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc