1
1
import { getAssetFromKV , mapRequestToAsset } from '@cloudflare/kv-asset-handler'
2
2
3
+ /* globals GA, GA_ACCESS_TOKEN, SITEMAP, DISCOVERY_NODES, HTMLRewriter */
4
+
3
5
const DEBUG = false
4
6
7
+ const discoveryNodes = DISCOVERY_NODES . split ( ',' )
8
+ const discoveryNode =
9
+ discoveryNodes [ Math . floor ( Math . random ( ) * discoveryNodes . length ) ]
10
+
11
+ const routes = [
12
+ { pattern : / ^ \/ ( [ ^ / ] + ) $ / , name : 'user' , keys : [ 'handle' ] } ,
13
+ {
14
+ pattern : / ^ \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) $ / ,
15
+ name : 'track' ,
16
+ keys : [ 'handle' , 'title' ]
17
+ } ,
18
+ {
19
+ pattern : / ^ \/ ( [ ^ / ] + ) \/ p l a y l i s t \/ ( [ ^ / ] + ) $ / ,
20
+ name : 'playlist' ,
21
+ keys : [ 'handle' , 'title' ]
22
+ } ,
23
+ {
24
+ pattern : / ^ \/ ( [ ^ / ] + ) \/ a l b u m \/ ( [ ^ / ] + ) $ / ,
25
+ name : 'album' ,
26
+ keys : [ 'handle' , 'title' ]
27
+ }
28
+ ]
29
+
5
30
addEventListener ( 'fetch' , ( event ) => {
6
31
try {
7
32
event . respondWith ( handleEvent ( event ) )
@@ -17,17 +42,175 @@ addEventListener('fetch', (event) => {
17
42
}
18
43
} )
19
44
45
+ function matchRoute ( input ) {
46
+ for ( const route of routes ) {
47
+ const match = route . pattern . exec ( input )
48
+ if ( match ) {
49
+ const result = { name : route . name , params : { } }
50
+ route . keys . forEach ( ( key , index ) => {
51
+ result . params [ key ] = match [ index + 1 ]
52
+ } )
53
+ return result
54
+ }
55
+ }
56
+ return null
57
+ }
58
+
20
59
function checkIsBot ( val ) {
21
60
if ( ! val ) {
22
61
return false
23
62
}
24
- const botTest = new RegExp (
25
- 'altavista|baiduspider|bingbot|discordbot|duckduckbot|facebookexternalhit|gigabot|ia_archiver|linkbot|linkedinbot|msnbot|nextgensearchbot|reaper|slackbot|snap url preview service|telegrambot|twitterbot|whatsapp|whatsup|yahoo|yandex|yeti|yodaobot|zend|zoominfobot|embedly' ,
26
- 'i'
27
- )
63
+ const botTest =
64
+ / a l t a v i s t a | b a i d u s p i d e r | b i n g b o t | d i s c o r d b o t | d u c k d u c k b o t | f a c e b o o k e x t e r n a l h i t | g i g a b o t | i a _ a r c h i v e r | l i n k b o t | l i n k e d i n b o t | m s n b o t | n e x t g e n s e a r c h b o t | r e a p e r | s l a c k b o t | s n a p u r l p r e v i e w s e r v i c e | t e l e g r a m b o t | t w i t t e r b o t | w h a t s a p p | w h a t s u p | y a h o o | y a n d e x | y e t i | y o d a o b o t | z e n d | z o o m i n f o b o t | e m b e d l y / i
28
65
return botTest . test ( val )
29
66
}
30
67
68
+ async function getMetadata ( pathname ) {
69
+ if ( pathname . startsWith ( '/scripts' ) ) {
70
+ return { metadata : null , name : null }
71
+ }
72
+
73
+ const route = matchRoute ( pathname )
74
+ if ( ! route ) {
75
+ return { metadata : null , name : null }
76
+ }
77
+
78
+ let discoveryRequestPath
79
+ switch ( route . name ) {
80
+ case 'user' : {
81
+ const { handle } = route . params
82
+ if ( ! handle ) return { metadata : null , name : null }
83
+ discoveryRequestPath = `v1/users/handle/${ handle } `
84
+ break
85
+ }
86
+ case 'track' : {
87
+ const { handle, title } = route . params
88
+ if ( ! handle || ! title ) return { metadata : null , name : null }
89
+ discoveryRequestPath = `v1/tracks?handle=${ handle } &slug=${ title } `
90
+ break
91
+ }
92
+ case 'playlist' : {
93
+ const { handle, title } = route . params
94
+ if ( ! handle || ! title ) return { metadata : null , name : null }
95
+ discoveryRequestPath = `v1/resolve?url=${ pathname } `
96
+ // TODO: Uncomment when by_permalink routes are working properly
97
+ // discoveryRequestPath = `v1/full/playlists/by_permalink/${handle}/${title}`
98
+ break
99
+ }
100
+ case 'album' : {
101
+ const { handle, title } = route . params
102
+ if ( ! handle || ! title ) return { metadata : null , name : null }
103
+ discoveryRequestPath = `v1/resolve?url=${ pathname } `
104
+ // TODO: Uncomment when by_permalink routes are working properly
105
+ // discoveryRequestPath = `v1/full/playlists/by_permalink/${handle}/${title}`
106
+ break
107
+ }
108
+ default :
109
+ return { metadata : null , name : null }
110
+ }
111
+ try {
112
+ const res = await fetch ( `${ discoveryNode } /${ discoveryRequestPath } ` )
113
+ if ( res . status !== 200 ) {
114
+ throw new Error ( res . status )
115
+ }
116
+ const json = await res . json ( )
117
+ return { metadata : json , name : route . name }
118
+ } catch ( e ) {
119
+ return { metadata : null , name : null }
120
+ }
121
+ }
122
+
123
+ function clean ( str ) {
124
+ return String ( str )
125
+ . replace ( / & / g, '&' )
126
+ . replace ( / < / g, '<' )
127
+ . replace ( / > / g, '>' )
128
+ . replace ( / " / g, '"' )
129
+ }
130
+
131
+ class HeadElementHandler {
132
+ constructor ( pathname ) {
133
+ self . pathname = pathname
134
+ }
135
+
136
+ async element ( element ) {
137
+ const { metadata, name } = await getMetadata ( self . pathname )
138
+
139
+ if ( ! metadata || ! name ) {
140
+ // We didn't parse this to anything we have custom tags for, so just return the default tags
141
+ const tags = `<meta property="og:title" content="Audius - Empowering Creators">
142
+ <meta name="description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators." data-react-helmet="true">
143
+ <meta property="og:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
144
+ <meta property="og:image" content="https://audius.co/ogImage.jpg">
145
+ <meta name="twitter:title" content="Audius - Empowering Creators">
146
+ <meta name="twitter:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
147
+ <meta name="twitter:image" content="https://audius.co/ogImage.jpg">
148
+ <meta name="twitter:image:alt" content="The Audius Platform">`
149
+ element . append ( tags , { html : true } )
150
+ return
151
+ }
152
+
153
+ let title , description , ogDescription , image , permalink
154
+ switch ( name ) {
155
+ case 'user' : {
156
+ title = `Stream ${ metadata . data . name } on Audius`
157
+ description = `Play ${ metadata . data . name } on Audius | Listen to tracks, albums, playlists on desktop and mobile on Audius.`
158
+ ogDescription = metadata . data . bio || description
159
+ image = metadata . data . profile_picture
160
+ ? metadata . data . profile_picture [ '1000x1000' ]
161
+ : ''
162
+ permalink = `/${ metadata . data . handle } `
163
+ break
164
+ }
165
+ case 'track' : {
166
+ description = `Stream ${ metadata . data . title } by ${ metadata . data . user . name } on Audius. Listen on desktop and mobile.`
167
+ title = `${ metadata . data . title } | Stream ${ metadata . data . user . name } `
168
+ ogDescription = metadata . data . description || description
169
+ image = metadata . data . artwork ? metadata . data . artwork [ '1000x1000' ] : ''
170
+ permalink = metadata . data . permalink
171
+ break
172
+ }
173
+ case 'playlist' : {
174
+ description = `Listen to ${ metadata . data [ 0 ] . playlist_name } , a playlist curated by ${ metadata . data [ 0 ] . user . name } on desktop and mobile.`
175
+ title = `${ metadata . data [ 0 ] . playlist_name } | Playlist by ${ metadata . data [ 0 ] . user . name } `
176
+ ogDescription = metadata . data [ 0 ] . description || ''
177
+ image = metadata . data [ 0 ] . artwork
178
+ ? metadata . data [ 0 ] . artwork [ '1000x1000' ]
179
+ : ''
180
+ permalink = metadata . data [ 0 ] . permalink
181
+ break
182
+ }
183
+ case 'album' : {
184
+ description = `Listen to ${ metadata . data [ 0 ] . playlist_name } , a playlist curated by ${ metadata . data [ 0 ] . user . name } on desktop and mobile.`
185
+ title = `${ metadata . data [ 0 ] . playlist_name } | Playlist by ${ metadata . data [ 0 ] . user . name } `
186
+ ogDescription = metadata . data [ 0 ] . description || ''
187
+ image = metadata . data [ 0 ] . artwork
188
+ ? metadata . data [ 0 ] . artwork [ '1000x1000' ]
189
+ : ''
190
+ permalink = metadata . data [ 0 ] . permalink
191
+ break
192
+ }
193
+ default :
194
+ return
195
+ }
196
+ const tags = `<title>${ clean ( title ) } </title>
197
+ <meta name="description" content="${ clean ( description ) } ">
198
+
199
+ <link rel="canonical" href="https://audius.co${ permalink } ">
200
+
201
+ <meta property="og:title" content="${ clean ( title ) } ">
202
+ <meta property="og:description" content="${ clean ( ogDescription ) } ">
203
+ <meta property="og:image" content="${ image } ">
204
+ <meta property="og:url" content="https://audius.co${ permalink } ">
205
+
206
+ <meta name="twitter:card" content="summary">
207
+ <meta name="twitter:title" content="${ clean ( title ) } ">
208
+ <meta name="twitter:description" content="${ clean ( ogDescription ) } ">
209
+ <meta name="twitter:image" content=https://audius.co${ permalink } ">`
210
+ element . append ( tags , { html : true } )
211
+ }
212
+ }
213
+
31
214
async function handleEvent ( event ) {
32
215
const url = new URL ( event . request . url )
33
216
const { pathname, search, hash } = url
@@ -82,7 +265,12 @@ async function handleEvent(event) {
82
265
}
83
266
}
84
267
85
- return await getAssetFromKV ( event , options )
268
+ const asset = await getAssetFromKV ( event , options )
269
+
270
+ const rewritten = new HTMLRewriter ( )
271
+ . on ( 'head' , new HeadElementHandler ( pathname ) )
272
+ . transform ( asset )
273
+ return rewritten
86
274
} catch ( e ) {
87
275
return new Response ( e . message || e . toString ( ) , { status : 500 } )
88
276
}
0 commit comments