Skip to content

Commit 20476ee

Browse files
[C-2941] Modify cloudflare worker to pull in SEO data from discovery nodes (#3858)
Co-authored-by: Sebastian Klingler <[email protected]>
1 parent 7f79830 commit 20476ee

File tree

4 files changed

+204
-20
lines changed

4 files changed

+204
-20
lines changed

packages/web/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
/coverage
1515

1616
# build
17+
/sourcemaps
1718
/build
1819
/build-development
1920
/build-demo

packages/web/public/index.html

+4-15
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,13 @@
2121

2222
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
2323

24-
<!-- SEO -->
25-
<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">
26-
27-
<!-- Social -->
28-
<meta property="og:title" content="Audius - Empowering Creators">
24+
<meta name="application-name" content="Audius">
2925
<meta property="og:site_name" content="Audius">
30-
<meta property="og:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
31-
<meta property="og:image" content="%PUBLIC_URL%/ogImage.jpg">
3226
<meta property="fb:app_id" content="123553997750078" />
33-
34-
<meta name="twitter:title" content="Audius - Empowering Creators">
35-
<meta name="twitter:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
36-
<meta name="twitter:image" content="%PUBLIC_URL%/ogImage.jpg">
37-
<meta name="twitter:image:alt" content="The Audius Platform">
3827
<meta name="twitter:card" content="summary_large_image">
3928
<meta name="twitter:site" content="@AudiusProject">
29+
<meta property="twitter:app:name:iphone" content="Audius Music">
30+
<meta property="twitter:app:name:googleplay" content="Audius Music">
4031

4132
<link rel="preconnect" href="https://www.google-analytics.com" crossorigin>
4233

@@ -46,8 +37,6 @@
4637
src="%PUBLIC_URL%/scripts/web3.min.js"
4738
></script>
4839

49-
<title>Audius</title>
50-
5140
<script async type="text/javascript">
5241
// Account recovery
5342
try{
@@ -98,7 +87,7 @@
9887
<!-- end Google Analytics -->
9988

10089
<!-- start Adroll -->
101-
<script type="text/javascript">
90+
<script async type="text/javascript">
10291
const adroll_adv_id = '%REACT_APP_ADROLL_AVD_ID%';
10392
const adroll_pix_id = '%REACT_APP_ADROLL_PIX_ID%';
10493
const adroll_version = "2.0";

packages/web/scripts/workers-site/index.js

+193-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
11
import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler'
22

3+
/* globals GA, GA_ACCESS_TOKEN, SITEMAP, DISCOVERY_NODES, HTMLRewriter */
4+
35
const DEBUG = false
46

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: /^\/([^/]+)\/playlist\/([^/]+)$/,
20+
name: 'playlist',
21+
keys: ['handle', 'title']
22+
},
23+
{
24+
pattern: /^\/([^/]+)\/album\/([^/]+)$/,
25+
name: 'album',
26+
keys: ['handle', 'title']
27+
}
28+
]
29+
530
addEventListener('fetch', (event) => {
631
try {
732
event.respondWith(handleEvent(event))
@@ -17,17 +42,175 @@ addEventListener('fetch', (event) => {
1742
}
1843
})
1944

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+
2059
function checkIsBot(val) {
2160
if (!val) {
2261
return false
2362
}
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+
/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/i
2865
return botTest.test(val)
2966
}
3067

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, '&amp;')
126+
.replace(/</g, '&lt;')
127+
.replace(/>/g, '&gt;')
128+
.replace(/"/g, '&quot;')
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+
31214
async function handleEvent(event) {
32215
const url = new URL(event.request.url)
33216
const { pathname, search, hash } = url
@@ -82,7 +265,12 @@ async function handleEvent(event) {
82265
}
83266
}
84267

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
86274
} catch (e) {
87275
return new Response(e.message || e.toString(), { status: 500 })
88276
}

packages/web/wrangler.toml

+6
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ vars = { ENVIRONMENT = "production", GA = "https://general-admission.audius.co",
2020

2121
[env.production]
2222
name = "audius"
23+
vars = { ENVIRONMENT = "production", GA = "https://general-admission.audius.co", SITEMAP = "http://audius.co.s3-website-us-west-1.amazonaws.com" }
24+
25+
# Test environment, replace `test` with subdomain
26+
# Invoke with npx wrangler preview --watch --env test
27+
[env.test]
28+
name = "test"
2329
vars = { ENVIRONMENT = "production", GA = "https://general-admission.audius.co", SITEMAP = "http://audius.co.s3-website-us-west-1.amazonaws.com" }

0 commit comments

Comments
 (0)