@xylabs/sdk-meta 4.0.7
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/LICENSE +165 -0
- package/README.md +62 -0
- package/dist/neutral/html/index.d.ts +2 -0
- package/dist/neutral/html/index.d.ts.map +1 -0
- package/dist/neutral/html/mergeDocumentHead.d.ts +2 -0
- package/dist/neutral/html/mergeDocumentHead.d.ts.map +1 -0
- package/dist/neutral/index.d.ts +4 -0
- package/dist/neutral/index.d.ts.map +1 -0
- package/dist/neutral/index.mjs +87 -0
- package/dist/neutral/index.mjs.map +1 -0
- package/dist/neutral/lib/getMetaAsDict.d.ts +6 -0
- package/dist/neutral/lib/getMetaAsDict.d.ts.map +1 -0
- package/dist/neutral/lib/index.d.ts +2 -0
- package/dist/neutral/lib/index.d.ts.map +1 -0
- package/dist/neutral/meta/builder.d.ts +3 -0
- package/dist/neutral/meta/builder.d.ts.map +1 -0
- package/dist/neutral/meta/index.d.ts +2 -0
- package/dist/neutral/meta/index.d.ts.map +1 -0
- package/dist/neutral/models/Meta.d.ts +9 -0
- package/dist/neutral/models/Meta.d.ts.map +1 -0
- package/dist/neutral/models/OpenGraph/OpenGraphMeta.d.ts +14 -0
- package/dist/neutral/models/OpenGraph/OpenGraphMeta.d.ts.map +1 -0
- package/dist/neutral/models/OpenGraph/OpenGraphStructured.d.ts +10 -0
- package/dist/neutral/models/OpenGraph/OpenGraphStructured.d.ts.map +1 -0
- package/dist/neutral/models/OpenGraph/OpenGraphStructuredProperty.d.ts +3 -0
- package/dist/neutral/models/OpenGraph/OpenGraphStructuredProperty.d.ts.map +1 -0
- package/dist/neutral/models/OpenGraph/index.d.ts +4 -0
- package/dist/neutral/models/OpenGraph/index.d.ts.map +1 -0
- package/dist/neutral/models/Twitter/Twitter.d.ts +22 -0
- package/dist/neutral/models/Twitter/Twitter.d.ts.map +1 -0
- package/dist/neutral/models/Twitter/TwitterApp.d.ts +18 -0
- package/dist/neutral/models/Twitter/TwitterApp.d.ts.map +1 -0
- package/dist/neutral/models/Twitter/TwitterPlayer.d.ts +7 -0
- package/dist/neutral/models/Twitter/TwitterPlayer.d.ts.map +1 -0
- package/dist/neutral/models/Twitter/index.d.ts +4 -0
- package/dist/neutral/models/Twitter/index.d.ts.map +1 -0
- package/dist/neutral/models/index.d.ts +4 -0
- package/dist/neutral/models/index.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/html/index.ts +1 -0
- package/src/html/mergeDocumentHead.ts +46 -0
- package/src/html/spec/__snapshots__/mergeDocumentHead.approval.spec.ts.snap +45 -0
- package/src/html/spec/destination.html +23 -0
- package/src/html/spec/mergeDocumentHead.approval.spec.ts +20 -0
- package/src/html/spec/mergeDocumentHead.spec.ts +46 -0
- package/src/html/spec/source.html +2724 -0
- package/src/index.ts +3 -0
- package/src/lib/getMetaAsDict.ts +21 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/spec/getMetaAsDict.spec.ts +16 -0
- package/src/meta/builder.ts +49 -0
- package/src/meta/index.ts +1 -0
- package/src/models/Meta.ts +9 -0
- package/src/models/OpenGraph/OpenGraphMeta.ts +17 -0
- package/src/models/OpenGraph/OpenGraphStructured.ts +9 -0
- package/src/models/OpenGraph/OpenGraphStructuredProperty.ts +3 -0
- package/src/models/OpenGraph/index.ts +3 -0
- package/src/models/Twitter/Twitter.ts +67 -0
- package/src/models/Twitter/TwitterApp.ts +17 -0
- package/src/models/Twitter/TwitterPlayer.ts +23 -0
- package/src/models/Twitter/index.ts +3 -0
- package/src/models/index.ts +3 -0
- package/src/spec/__snapshots__/builder.spec.ts.snap +187 -0
- package/src/spec/builder.spec.ts +125 -0
- package/src/spec/template.html +42 -0
- package/typedoc.json +5 -0
- package/xy.config.ts +10 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
export type StringIndexable = { [key: string]: any }
|
|
3
|
+
|
|
4
|
+
export const propertyDelimiter = ':'
|
|
5
|
+
|
|
6
|
+
export const getMetaAsDict = (obj: StringIndexable, parentKey = ''): Record<string, string> => {
|
|
7
|
+
let flatRecord: StringIndexable = {}
|
|
8
|
+
for (const key in obj) {
|
|
9
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
10
|
+
// If the value is another object, we want to iterate through its keys as well.
|
|
11
|
+
const childRecord = getMetaAsDict(obj[key] as StringIndexable, `${parentKey}${key}${propertyDelimiter}`)
|
|
12
|
+
flatRecord = { ...flatRecord, ...childRecord }
|
|
13
|
+
} else {
|
|
14
|
+
// Concatenate the key with its parent key.
|
|
15
|
+
const newKey = parentKey ? `${parentKey}${key}` : key
|
|
16
|
+
const trimmed = newKey.endsWith(propertyDelimiter) ? newKey.slice(0, -1) : newKey
|
|
17
|
+
flatRecord[trimmed] = `${obj[key]}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return flatRecord
|
|
21
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './getMetaAsDict.ts'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Meta } from '../../models'
|
|
2
|
+
import { getMetaAsDict } from '../getMetaAsDict'
|
|
3
|
+
|
|
4
|
+
describe('getMetaAsDict', () => {
|
|
5
|
+
const cases: Meta[] = [
|
|
6
|
+
{ description: 'description' },
|
|
7
|
+
{ twitter: { image: { '': 'twitter:image' } } },
|
|
8
|
+
]
|
|
9
|
+
it.each(cases)('Generates head meta', (meta: Meta) => {
|
|
10
|
+
const output = getMetaAsDict(meta)
|
|
11
|
+
expect(output).toBeDefined()
|
|
12
|
+
Object.entries(output).map(([key, value]) => {
|
|
13
|
+
expect(key).toEqual(value)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CheerioAPI } from 'cheerio'
|
|
2
|
+
import { load } from 'cheerio'
|
|
3
|
+
|
|
4
|
+
import { getMetaAsDict } from '../lib/index.ts'
|
|
5
|
+
import type { Meta } from '../models/index.ts'
|
|
6
|
+
|
|
7
|
+
/* test change */
|
|
8
|
+
|
|
9
|
+
const addMetaToHead = ($: CheerioAPI, name: string, value: string | object) => {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
const newMeta = `<meta property="${name}" content="${value}" />`
|
|
12
|
+
const existingMeta = $(`head meta[property="${name}"]`)
|
|
13
|
+
if (existingMeta?.length) {
|
|
14
|
+
existingMeta.replaceWith(newMeta)
|
|
15
|
+
} else {
|
|
16
|
+
$('head').append(newMeta)
|
|
17
|
+
}
|
|
18
|
+
} else if (Array.isArray(value)) {
|
|
19
|
+
value.map(item => addMetaToHead($, `${name}`, item))
|
|
20
|
+
} else if (typeof value === 'object') {
|
|
21
|
+
Object.entries(value).map(([key, value]) => {
|
|
22
|
+
if (key === 'url') {
|
|
23
|
+
addMetaToHead($, name, value)
|
|
24
|
+
} else {
|
|
25
|
+
addMetaToHead($, `${name}:${key}`, value)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
} else {
|
|
29
|
+
throw new TypeError(`Invalid item type [${name}, ${typeof value}]`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const metaBuilder = (html: string, meta: Meta) => {
|
|
34
|
+
const $ = load(html)
|
|
35
|
+
// NOTE: This assumes unique meta properties (no duplicates)
|
|
36
|
+
// which is generally the case, but not always (you can have
|
|
37
|
+
// multiple og:video:tag tags, for example)
|
|
38
|
+
const metaProperties = getMetaAsDict(meta)
|
|
39
|
+
for (const [key, value] of Object.entries(metaProperties)) {
|
|
40
|
+
if (value) addMetaToHead($, key, value)
|
|
41
|
+
}
|
|
42
|
+
if (meta.description) {
|
|
43
|
+
addMetaToHead($, 'description', meta.description)
|
|
44
|
+
}
|
|
45
|
+
if (meta.title) {
|
|
46
|
+
$('head > title').text(meta.title)
|
|
47
|
+
}
|
|
48
|
+
return $.html()
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './builder.ts'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenGraphStructuredProperty } from './OpenGraphStructuredProperty.ts'
|
|
2
|
+
|
|
3
|
+
// TODO: There is slightly different fields for video/image/audio and we should create
|
|
4
|
+
// separate interfaces for each
|
|
5
|
+
|
|
6
|
+
export interface OpenGraphMeta {
|
|
7
|
+
audio?: OpenGraphStructuredProperty
|
|
8
|
+
description?: string
|
|
9
|
+
determiner?: string
|
|
10
|
+
image?: OpenGraphStructuredProperty
|
|
11
|
+
locale?: string | string[]
|
|
12
|
+
site_name?: string
|
|
13
|
+
title?: string
|
|
14
|
+
type?: string
|
|
15
|
+
url?: string
|
|
16
|
+
video?: OpenGraphStructuredProperty
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { TwitterApp } from './TwitterApp.ts'
|
|
2
|
+
import type { TwitterPlayer } from './TwitterPlayer.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup
|
|
6
|
+
*/
|
|
7
|
+
export interface TwitterMeta {
|
|
8
|
+
app?: TwitterApp
|
|
9
|
+
/**
|
|
10
|
+
* The card type. Used with all cards. Fallback: og:type.
|
|
11
|
+
* If an og:type, og:title and og:description exist in the markup but
|
|
12
|
+
* twitter:card is absent, then a summary card may be rendered.
|
|
13
|
+
*/
|
|
14
|
+
card?: 'summary' | 'summary_large_image' | 'app' | 'player'
|
|
15
|
+
|
|
16
|
+
creator?: {
|
|
17
|
+
/**
|
|
18
|
+
* The @username of content creator. Used with summary_large_image cards
|
|
19
|
+
*/
|
|
20
|
+
''?: string
|
|
21
|
+
/**
|
|
22
|
+
* Twitter user ID of content creator. Used with summary,
|
|
23
|
+
* summary_large_image cards
|
|
24
|
+
*/
|
|
25
|
+
'id'?: string
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Description of content (maximum 200 characters). Used with summary,
|
|
29
|
+
* summary_large_image, player cards. Fallback: og:description.
|
|
30
|
+
*/
|
|
31
|
+
description?: string
|
|
32
|
+
image?: {
|
|
33
|
+
/**
|
|
34
|
+
* URL of image to use in the card. Images must be less than 5MB in size.
|
|
35
|
+
* JPG, PNG, WEBP and GIF formats are supported. Only the first frame of
|
|
36
|
+
* an animated GIF will be used. SVG is not supported. Used with summary,
|
|
37
|
+
* summary_large_image, player cards. Fallback: og:image
|
|
38
|
+
*/
|
|
39
|
+
''?: string
|
|
40
|
+
/**
|
|
41
|
+
* A text description of the image conveying the essential nature of
|
|
42
|
+
* an image to users who are visually impaired. Maximum 420
|
|
43
|
+
* characters. Used with summary, summary_large_image, player cards
|
|
44
|
+
*/
|
|
45
|
+
'alt'?: string
|
|
46
|
+
}
|
|
47
|
+
player?: TwitterPlayer
|
|
48
|
+
/**
|
|
49
|
+
* The @username of website. Either twitter:site or twitter:site:id is
|
|
50
|
+
* required. Used with summary, summary_large_image, app, player
|
|
51
|
+
* cards
|
|
52
|
+
*/
|
|
53
|
+
site?: {
|
|
54
|
+
''?: string
|
|
55
|
+
/**
|
|
56
|
+
* Same as twitter:site, but the user’s Twitter ID. Either
|
|
57
|
+
* twitter:site or twitter:site:id is required. Used with
|
|
58
|
+
* summary, summary_large_image, player cards
|
|
59
|
+
*/
|
|
60
|
+
'id'?: string
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Title of content (max 70 characters). Used with summary,
|
|
64
|
+
* summary_large_image, player cards. Fallback: og:title.
|
|
65
|
+
*/
|
|
66
|
+
title?: string
|
|
67
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TwitterApp {
|
|
2
|
+
id?: {
|
|
3
|
+
googleplay?: string
|
|
4
|
+
ipad?: string
|
|
5
|
+
iphone?: string
|
|
6
|
+
}
|
|
7
|
+
name?: {
|
|
8
|
+
googleplay?: string
|
|
9
|
+
ipad?: string
|
|
10
|
+
iphone?: string
|
|
11
|
+
}
|
|
12
|
+
url?: {
|
|
13
|
+
googleplay?: string
|
|
14
|
+
ipad?: string
|
|
15
|
+
iphone?: string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/player-card
|
|
3
|
+
*/
|
|
4
|
+
export interface TwitterPlayer {
|
|
5
|
+
/**
|
|
6
|
+
* HTTPS URL to iFrame player. This must be a HTTPS URL which does not
|
|
7
|
+
* generate active mixed content warnings in a web browser. The audio or
|
|
8
|
+
* video player must not require plugins such as Adobe Flash.
|
|
9
|
+
*/
|
|
10
|
+
'': string
|
|
11
|
+
/**
|
|
12
|
+
* Height of iframe in pixels. Used with player card
|
|
13
|
+
*/
|
|
14
|
+
'height'?: number
|
|
15
|
+
/**
|
|
16
|
+
* URL to raw video or audio stream. Used with player card
|
|
17
|
+
*/
|
|
18
|
+
'stream'?: string
|
|
19
|
+
/**
|
|
20
|
+
* Width of iframe in pixels. Used with player card
|
|
21
|
+
*/
|
|
22
|
+
'width'?: number
|
|
23
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`builder > Adds meta > Open Graph (OG) 1`] = `
|
|
4
|
+
"<!DOCTYPE html><html lang="en"><head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<link rel="icon" href="/favicon.ico">
|
|
7
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8
|
+
<meta name="theme-color" content="#000000">
|
|
9
|
+
<meta name="description" content="Own your piece of XYO's Decentralized Digital World!">
|
|
10
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
11
|
+
<link rel="manifest" href="/manifest.json">
|
|
12
|
+
<title>Death Valley Wilderness: Wilderness Light</title>
|
|
13
|
+
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans|Lexend+Deca|Rock+Salt|Source+Code+Pro&display=swap" rel="stylesheet">
|
|
14
|
+
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-795QBPW744"></script>
|
|
15
|
+
<script>function gtag() { dataLayer.push(arguments) } window.dataLayer = window.dataLayer || [], gtag("js", new Date), gtag("config", "G-795QBPW744")</script>
|
|
16
|
+
<style>
|
|
17
|
+
html {
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
overflow-x: hidden
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#root,
|
|
23
|
+
body {
|
|
24
|
+
height: 100%;
|
|
25
|
+
width: 100%;
|
|
26
|
+
margin: 0;
|
|
27
|
+
padding: 0
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
<script defer="defer" src="/static/js/main.ae7f7033.js"></script>
|
|
31
|
+
<link href="/static/css/main.026e3fe6.css" rel="stylesheet">
|
|
32
|
+
<meta property="description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="title" content="Death Valley Wilderness: Wilderness Light"><meta property="og:description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="og:image" content="https://i.ytimg.com/vi/Kauv7MVPcsA/maxresdefault.jpg"><meta property="og:image:height" content="720"><meta property="og:image:width" content="1280"><meta property="og:site_name" content="YouTube"><meta property="og:title" content="Death Valley Wilderness: Wilderness Light"><meta property="og:type" content="video.other"><meta property="og:url" content="https://www.youtube.com/watch?v=Kauv7MVPcsA"><meta property="og:video:height" content="720"><meta property="og:video:secure_url" content="https://www.youtube.com/embed/Kauv7MVPcsA"><meta property="og:video:type" content="text/html"><meta property="og:video:url" content="https://www.youtube.com/embed/Kauv7MVPcsA"><meta property="og:video:width" content="1280"></head>
|
|
33
|
+
|
|
34
|
+
<body style="padding:0;margin:0;overflow-x:hidden"><noscript><iframe
|
|
35
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-W2TFNXL" height="0" width="0"
|
|
36
|
+
style="display:none;visibility:hidden"></iframe></noscript><noscript>You need to enable JavaScript to run this
|
|
37
|
+
app.</noscript>
|
|
38
|
+
<div id="root"></div>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
</body></html>"
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
exports[`builder > Adds meta > Twitter 1`] = `
|
|
45
|
+
"<!DOCTYPE html><html lang="en"><head>
|
|
46
|
+
<meta charset="utf-8">
|
|
47
|
+
<link rel="icon" href="/favicon.ico">
|
|
48
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
49
|
+
<meta name="theme-color" content="#000000">
|
|
50
|
+
<meta name="description" content="Own your piece of XYO's Decentralized Digital World!">
|
|
51
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
52
|
+
<link rel="manifest" href="/manifest.json">
|
|
53
|
+
<title>Death Valley Wilderness: Wilderness Light</title>
|
|
54
|
+
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans|Lexend+Deca|Rock+Salt|Source+Code+Pro&display=swap" rel="stylesheet">
|
|
55
|
+
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-795QBPW744"></script>
|
|
56
|
+
<script>function gtag() { dataLayer.push(arguments) } window.dataLayer = window.dataLayer || [], gtag("js", new Date), gtag("config", "G-795QBPW744")</script>
|
|
57
|
+
<style>
|
|
58
|
+
html {
|
|
59
|
+
overflow-y: auto;
|
|
60
|
+
overflow-x: hidden
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#root,
|
|
64
|
+
body {
|
|
65
|
+
height: 100%;
|
|
66
|
+
width: 100%;
|
|
67
|
+
margin: 0;
|
|
68
|
+
padding: 0
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
<script defer="defer" src="/static/js/main.ae7f7033.js"></script>
|
|
72
|
+
<link href="/static/css/main.026e3fe6.css" rel="stylesheet">
|
|
73
|
+
<meta property="description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="title" content="Death Valley Wilderness: Wilderness Light"><meta property="twitter:app:id:googleplay" content="com.google.android.youtube"><meta property="twitter:app:id:ipad" content="544007664"><meta property="twitter:app:id:iphone" content="544007664"><meta property="twitter:app:name:googleplay" content="YouTube"><meta property="twitter:app:name:ipad" content="YouTube"><meta property="twitter:app:name:iphone" content="YouTube"><meta property="twitter:app:url:googleplay" content="https://www.youtube.com/watch?v=Kauv7MVPcsA"><meta property="twitter:app:url:ipad" content="vnd.youtube://www.youtube.com/watch?v=Kauv7MVPcsA&feature=applinks"><meta property="twitter:app:url:iphone" content="vnd.youtube://www.youtube.com/watch?v=Kauv7MVPcsA&feature=applinks"><meta property="twitter:card" content="summary"><meta property="twitter:description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="twitter:image" content="https://www.twitter.com/image/url"><meta property="twitter:player" content="https://www.youtube.com/watch?v=Kauv7MVPcsA"><meta property="twitter:player:height" content="720"><meta property="twitter:player:width" content="1028"><meta property="twitter:site" content="@youtube"><meta property="twitter:title" content="Death Valley Wilderness: Wilderness Light"></head>
|
|
74
|
+
|
|
75
|
+
<body style="padding:0;margin:0;overflow-x:hidden"><noscript><iframe
|
|
76
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-W2TFNXL" height="0" width="0"
|
|
77
|
+
style="display:none;visibility:hidden"></iframe></noscript><noscript>You need to enable JavaScript to run this
|
|
78
|
+
app.</noscript>
|
|
79
|
+
<div id="root"></div>
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
</body></html>"
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
exports[`builder > Overwrites existing values with new values 1`] = `
|
|
86
|
+
"<!DOCTYPE html><html lang="en"><head>
|
|
87
|
+
<title>XYO 2.0</title>
|
|
88
|
+
<meta property="og:image" content="https://example.com/newimage.jpg">
|
|
89
|
+
<meta property="twitter:image" content="https://example.com/newimage.jpg">
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
|
|
93
|
+
</body></html>"
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
exports[`builder Adds meta Open Graph (OG) 1`] = `
|
|
97
|
+
"<!DOCTYPE html><html lang="en"><head>
|
|
98
|
+
<meta charset="utf-8">
|
|
99
|
+
<link rel="icon" href="/favicon.ico">
|
|
100
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
101
|
+
<meta name="theme-color" content="#000000">
|
|
102
|
+
<meta name="description" content="Own your piece of XYO's Decentralized Digital World!">
|
|
103
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
104
|
+
<link rel="manifest" href="/manifest.json">
|
|
105
|
+
<title>Death Valley Wilderness: Wilderness Light</title>
|
|
106
|
+
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans|Lexend+Deca|Rock+Salt|Source+Code+Pro&display=swap" rel="stylesheet">
|
|
107
|
+
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-795QBPW744"></script>
|
|
108
|
+
<script>function gtag() { dataLayer.push(arguments) } window.dataLayer = window.dataLayer || [], gtag("js", new Date), gtag("config", "G-795QBPW744")</script>
|
|
109
|
+
<style>
|
|
110
|
+
html {
|
|
111
|
+
overflow-y: auto;
|
|
112
|
+
overflow-x: hidden
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#root,
|
|
116
|
+
body {
|
|
117
|
+
height: 100%;
|
|
118
|
+
width: 100%;
|
|
119
|
+
margin: 0;
|
|
120
|
+
padding: 0
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
123
|
+
<script defer="defer" src="/static/js/main.ae7f7033.js"></script>
|
|
124
|
+
<link href="/static/css/main.026e3fe6.css" rel="stylesheet">
|
|
125
|
+
<meta property="description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="title" content="Death Valley Wilderness: Wilderness Light"><meta property="og:description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="og:image" content="https://i.ytimg.com/vi/Kauv7MVPcsA/maxresdefault.jpg"><meta property="og:image:height" content="720"><meta property="og:image:width" content="1280"><meta property="og:site_name" content="YouTube"><meta property="og:title" content="Death Valley Wilderness: Wilderness Light"><meta property="og:type" content="video.other"><meta property="og:url" content="https://www.youtube.com/watch?v=Kauv7MVPcsA"><meta property="og:video:height" content="720"><meta property="og:video:secure_url" content="https://www.youtube.com/embed/Kauv7MVPcsA"><meta property="og:video:type" content="text/html"><meta property="og:video:url" content="https://www.youtube.com/embed/Kauv7MVPcsA"><meta property="og:video:width" content="1280"></head>
|
|
126
|
+
|
|
127
|
+
<body style="padding:0;margin:0;overflow-x:hidden"><noscript><iframe
|
|
128
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-W2TFNXL" height="0" width="0"
|
|
129
|
+
style="display:none;visibility:hidden"></iframe></noscript><noscript>You need to enable JavaScript to run this
|
|
130
|
+
app.</noscript>
|
|
131
|
+
<div id="root"></div>
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
</body></html>"
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
exports[`builder Adds meta Twitter 1`] = `
|
|
138
|
+
"<!DOCTYPE html><html lang="en"><head>
|
|
139
|
+
<meta charset="utf-8">
|
|
140
|
+
<link rel="icon" href="/favicon.ico">
|
|
141
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
142
|
+
<meta name="theme-color" content="#000000">
|
|
143
|
+
<meta name="description" content="Own your piece of XYO's Decentralized Digital World!">
|
|
144
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
145
|
+
<link rel="manifest" href="/manifest.json">
|
|
146
|
+
<title>Death Valley Wilderness: Wilderness Light</title>
|
|
147
|
+
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans|Lexend+Deca|Rock+Salt|Source+Code+Pro&display=swap" rel="stylesheet">
|
|
148
|
+
<script async="" src="https://www.googletagmanager.com/gtag/js?id=G-795QBPW744"></script>
|
|
149
|
+
<script>function gtag() { dataLayer.push(arguments) } window.dataLayer = window.dataLayer || [], gtag("js", new Date), gtag("config", "G-795QBPW744")</script>
|
|
150
|
+
<style>
|
|
151
|
+
html {
|
|
152
|
+
overflow-y: auto;
|
|
153
|
+
overflow-x: hidden
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#root,
|
|
157
|
+
body {
|
|
158
|
+
height: 100%;
|
|
159
|
+
width: 100%;
|
|
160
|
+
margin: 0;
|
|
161
|
+
padding: 0
|
|
162
|
+
}
|
|
163
|
+
</style>
|
|
164
|
+
<script defer="defer" src="/static/js/main.ae7f7033.js"></script>
|
|
165
|
+
<link href="/static/css/main.026e3fe6.css" rel="stylesheet">
|
|
166
|
+
<meta property="description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="title" content="Death Valley Wilderness: Wilderness Light"><meta property="twitter:app:id:googleplay" content="com.google.android.youtube"><meta property="twitter:app:id:ipad" content="544007664"><meta property="twitter:app:id:iphone" content="544007664"><meta property="twitter:app:name:googleplay" content="YouTube"><meta property="twitter:app:name:ipad" content="YouTube"><meta property="twitter:app:name:iphone" content="YouTube"><meta property="twitter:app:url:googleplay" content="https://www.youtube.com/watch?v=Kauv7MVPcsA"><meta property="twitter:app:url:ipad" content="vnd.youtube://www.youtube.com/watch?v=Kauv7MVPcsA&feature=applinks"><meta property="twitter:app:url:iphone" content="vnd.youtube://www.youtube.com/watch?v=Kauv7MVPcsA&feature=applinks"><meta property="twitter:card" content="summary"><meta property="twitter:description" content="Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w..."><meta property="twitter:image" content="https://www.twitter.com/image/url"><meta property="twitter:player" content="https://www.youtube.com/watch?v=Kauv7MVPcsA"><meta property="twitter:player:height" content="720"><meta property="twitter:player:width" content="1028"><meta property="twitter:site" content="@youtube"><meta property="twitter:title" content="Death Valley Wilderness: Wilderness Light"></head>
|
|
167
|
+
|
|
168
|
+
<body style="padding:0;margin:0;overflow-x:hidden"><noscript><iframe
|
|
169
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-W2TFNXL" height="0" width="0"
|
|
170
|
+
style="display:none;visibility:hidden"></iframe></noscript><noscript>You need to enable JavaScript to run this
|
|
171
|
+
app.</noscript>
|
|
172
|
+
<div id="root"></div>
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
</body></html>"
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
exports[`builder Overwrites existing values with new values 1`] = `
|
|
179
|
+
"<!DOCTYPE html><html lang="en"><head>
|
|
180
|
+
<title>XYO 2.0</title>
|
|
181
|
+
<meta property="og:image" content="https://example.com/newimage.jpg">
|
|
182
|
+
<meta property="twitter:image" content="https://example.com/newimage.jpg">
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
|
|
186
|
+
</body></html>"
|
|
187
|
+
`;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import Path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getMetaAsDict } from '../lib/index.ts'
|
|
5
|
+
import { metaBuilder } from '../meta/index.ts'
|
|
6
|
+
import type {
|
|
7
|
+
Meta, OpenGraphMeta, TwitterMeta,
|
|
8
|
+
} from '../models/index.ts'
|
|
9
|
+
|
|
10
|
+
const title = 'Death Valley Wilderness: Wilderness Light'
|
|
11
|
+
const description
|
|
12
|
+
// eslint-disable-next-line @stylistic/max-len
|
|
13
|
+
= 'Follow the course of light through the Death Valley Wilderness and observe the obvious and subtle changes across the landscape.Produced by Sylvia JohnsonTo w...'
|
|
14
|
+
|
|
15
|
+
const meta: Meta = { description, title }
|
|
16
|
+
|
|
17
|
+
const og: OpenGraphMeta = {
|
|
18
|
+
description,
|
|
19
|
+
image: {
|
|
20
|
+
'': 'https://i.ytimg.com/vi/Kauv7MVPcsA/maxresdefault.jpg',
|
|
21
|
+
'height': 720,
|
|
22
|
+
'width': 1280,
|
|
23
|
+
},
|
|
24
|
+
site_name: 'YouTube',
|
|
25
|
+
title,
|
|
26
|
+
type: 'video.other',
|
|
27
|
+
url: 'https://www.youtube.com/watch?v=Kauv7MVPcsA',
|
|
28
|
+
video: {
|
|
29
|
+
height: 720,
|
|
30
|
+
secure_url: 'https://www.youtube.com/embed/Kauv7MVPcsA',
|
|
31
|
+
// TODO: Multiple tags
|
|
32
|
+
// tag: ['Light', 'Death Valley'],
|
|
33
|
+
type: 'text/html',
|
|
34
|
+
url: 'https://www.youtube.com/embed/Kauv7MVPcsA',
|
|
35
|
+
width: 1280,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const twitter: TwitterMeta = {
|
|
40
|
+
app: {
|
|
41
|
+
id: {
|
|
42
|
+
googleplay: 'com.google.android.youtube',
|
|
43
|
+
ipad: '544007664',
|
|
44
|
+
iphone: '544007664',
|
|
45
|
+
},
|
|
46
|
+
name: {
|
|
47
|
+
googleplay: 'YouTube',
|
|
48
|
+
ipad: 'YouTube',
|
|
49
|
+
iphone: 'YouTube',
|
|
50
|
+
},
|
|
51
|
+
url: {
|
|
52
|
+
googleplay: 'https://www.youtube.com/watch?v=Kauv7MVPcsA',
|
|
53
|
+
ipad: 'vnd.youtube://www.youtube.com/watch?v=Kauv7MVPcsA&feature=applinks',
|
|
54
|
+
iphone: 'vnd.youtube://www.youtube.com/watch?v=Kauv7MVPcsA&feature=applinks',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
card: 'summary',
|
|
58
|
+
description,
|
|
59
|
+
image: { '': 'https://www.twitter.com/image/url' },
|
|
60
|
+
player: {
|
|
61
|
+
'': 'https://www.youtube.com/watch?v=Kauv7MVPcsA',
|
|
62
|
+
'height': 720,
|
|
63
|
+
'width': 1028,
|
|
64
|
+
},
|
|
65
|
+
site: { '': '@youtube' },
|
|
66
|
+
title,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('builder', () => {
|
|
70
|
+
it('Generates head meta if none exists', () => {
|
|
71
|
+
const html = '<html/>'
|
|
72
|
+
const output = metaBuilder(html, meta)
|
|
73
|
+
expect(output).toBeDefined()
|
|
74
|
+
})
|
|
75
|
+
describe('Adds meta', () => {
|
|
76
|
+
let html: string
|
|
77
|
+
beforeAll(async () => {
|
|
78
|
+
const template = Path.join(__dirname, 'template.html')
|
|
79
|
+
html = await readFile(template, { encoding: 'utf8' })
|
|
80
|
+
})
|
|
81
|
+
const cases: [title: string, meta: Meta][] = [
|
|
82
|
+
['Open Graph (OG)', { ...meta, og }],
|
|
83
|
+
['Twitter', { ...meta, twitter }],
|
|
84
|
+
]
|
|
85
|
+
it.each(cases)('%s', (_, meta) => {
|
|
86
|
+
const output = metaBuilder(html, meta)
|
|
87
|
+
expect(output).toContain(meta.description)
|
|
88
|
+
expect(output).toContain(`<title>${meta.title}</title>`)
|
|
89
|
+
const metaProperties = getMetaAsDict(meta)
|
|
90
|
+
for (const [property, content] of Object.entries(metaProperties)) {
|
|
91
|
+
expect(output).toContain(`<meta property="${property}" content="${content}">`)
|
|
92
|
+
}
|
|
93
|
+
expect(output).toMatchSnapshot()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
it('Overwrites existing values with new values', () => {
|
|
97
|
+
const oldImage = 'https://example.com/oldimage.jpg'
|
|
98
|
+
const newImage = 'https://example.com/newimage.jpg'
|
|
99
|
+
const html = `
|
|
100
|
+
<!doctype html>
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head>
|
|
103
|
+
<title>XYO 2.0</title>
|
|
104
|
+
<meta property="og:image" content="${oldImage}">
|
|
105
|
+
<meta property="twitter:image" content="${oldImage}">
|
|
106
|
+
</head>
|
|
107
|
+
<body></body>
|
|
108
|
+
</html>
|
|
109
|
+
`
|
|
110
|
+
const meta: Meta = {
|
|
111
|
+
og: { image: newImage },
|
|
112
|
+
twitter: { image: { '': newImage } },
|
|
113
|
+
}
|
|
114
|
+
const properties = ['og:image', 'twitter:image']
|
|
115
|
+
for (const property of properties) {
|
|
116
|
+
expect(html).toContain(`<meta property="${property}" content="${oldImage}">`)
|
|
117
|
+
}
|
|
118
|
+
const output = metaBuilder(html, meta)
|
|
119
|
+
for (const property of properties) {
|
|
120
|
+
expect(output).not.toContain(`<meta property="${property}" content="${oldImage}">`)
|
|
121
|
+
expect(output).toContain(`<meta property="${property}" content="${newImage}">`)
|
|
122
|
+
}
|
|
123
|
+
expect(output).toMatchSnapshot()
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<link rel="icon" href="/favicon.ico" />
|
|
7
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
8
|
+
<meta name="theme-color" content="#000000" />
|
|
9
|
+
<meta name="description" content="Own your piece of XYO's Decentralized Digital World!" />
|
|
10
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
11
|
+
<link rel="manifest" href="/manifest.json" />
|
|
12
|
+
<title>XYO 2.0</title>
|
|
13
|
+
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans|Lexend+Deca|Rock+Salt|Source+Code+Pro&display=swap"
|
|
14
|
+
rel="stylesheet">
|
|
15
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-795QBPW744"></script>
|
|
16
|
+
<script>function gtag() { dataLayer.push(arguments) } window.dataLayer = window.dataLayer || [], gtag("js", new Date), gtag("config", "G-795QBPW744")</script>
|
|
17
|
+
<style>
|
|
18
|
+
html {
|
|
19
|
+
overflow-y: auto;
|
|
20
|
+
overflow-x: hidden
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#root,
|
|
24
|
+
body {
|
|
25
|
+
height: 100%;
|
|
26
|
+
width: 100%;
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: 0
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
31
|
+
<script defer="defer" src="/static/js/main.ae7f7033.js"></script>
|
|
32
|
+
<link href="/static/css/main.026e3fe6.css" rel="stylesheet">
|
|
33
|
+
</head>
|
|
34
|
+
|
|
35
|
+
<body style="padding:0;margin:0;overflow-x:hidden"><noscript><iframe
|
|
36
|
+
src="https://www.googletagmanager.com/ns.html?id=GTM-W2TFNXL" height="0" width="0"
|
|
37
|
+
style="display:none;visibility:hidden"></iframe></noscript><noscript>You need to enable JavaScript to run this
|
|
38
|
+
app.</noscript>
|
|
39
|
+
<div id="root"></div>
|
|
40
|
+
</body>
|
|
41
|
+
|
|
42
|
+
</html>
|
package/typedoc.json
ADDED