Skip to content
This repository has been archived by the owner on Oct 4, 2023. It is now read-only.

[C-2941] Modify cloudflare worker to pull in SEO data from discovery nodes #3858

Merged
merged 3 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
/coverage

# build
/sourcemaps
/build
/build-development
/build-demo
Expand Down
20 changes: 5 additions & 15 deletions packages/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,13 @@

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

<!-- SEO -->
<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">

<!-- Social -->
<meta property="og:title" content="Audius - Empowering Creators">
<meta name="application-name" content="Audius">
<meta property="og:site_name" content="Audius">
<meta property="og:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
<meta property="og:image" content="%PUBLIC_URL%/ogImage.jpg">
<meta property="fb:app_id" content="123553997750078" />

<meta name="twitter:title" content="Audius - Empowering Creators">
<meta name="twitter:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
<meta name="twitter:image" content="%PUBLIC_URL%/ogImage.jpg">
<meta name="twitter:image:alt" content="The Audius Platform">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@AudiusProject">
<meta property="twitter:app:name:iphone" content="Audius Music">
<meta property="twitter:app:name:googleplay" content="Audius Music">

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

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

<title>Audius</title>

<script async type="text/javascript">
// Account recovery
try{
Expand Down Expand Up @@ -82,6 +71,7 @@
console.error(e)
}
</script>

<!-- start Google Analytics -->
<script async src="%PUBLIC_URL%/analytics/gtag.js?id=%REACT_APP_GA_MEASUREMENT_ID%"></script>
<script async>
Expand All @@ -102,7 +92,7 @@
<!-- end Google Analytics -->

<!-- start Adroll -->
<script type="text/javascript">
<script async type="text/javascript">
const adroll_adv_id = '%REACT_APP_ADROLL_AVD_ID%';
const adroll_pix_id = '%REACT_APP_ADROLL_PIX_ID%';
const adroll_version = "2.0";
Expand Down
198 changes: 193 additions & 5 deletions packages/web/scripts/workers-site/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler'

/* globals GA, GA_ACCESS_TOKEN, SITEMAP, DISCOVERY_NODES, HTMLRewriter */

const DEBUG = false

const discoveryNodes = DISCOVERY_NODES.split(',')
const discoveryNode =
discoveryNodes[Math.floor(Math.random() * discoveryNodes.length)]

const routes = [
{ pattern: /^\/([^/]+)$/, name: 'user', keys: ['handle'] },
{
pattern: /^\/([^/]+)\/([^/]+)$/,
name: 'track',
keys: ['handle', 'title']
},
{
pattern: /^\/([^/]+)\/playlist\/([^/]+)$/,
name: 'playlist',
keys: ['handle', 'title']
},
{
pattern: /^\/([^/]+)\/album\/([^/]+)$/,
name: 'album',
keys: ['handle', 'title']
}
]

addEventListener('fetch', (event) => {
try {
event.respondWith(handleEvent(event))
Expand All @@ -17,17 +42,175 @@ addEventListener('fetch', (event) => {
}
})

function matchRoute(input) {
for (const route of routes) {
const match = route.pattern.exec(input)
if (match) {
const result = { name: route.name, params: {} }
route.keys.forEach((key, index) => {
result.params[key] = match[index + 1]
})
return result
}
}
return null
}

function checkIsBot(val) {
if (!val) {
return false
}
const botTest = new RegExp(
'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'
)
const botTest =
/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
return botTest.test(val)
}

async function getMetadata(pathname) {
if (pathname.startsWith('/scripts')) {
return { metadata: null, name: null }
}

const route = matchRoute(pathname)
if (!route) {
return { metadata: null, name: null }
}

let discoveryRequestPath
switch (route.name) {
case 'user': {
const { handle } = route.params
if (!handle) return { metadata: null, name: null }
discoveryRequestPath = `v1/users/handle/${handle}`
break
}
case 'track': {
const { handle, title } = route.params
if (!handle || !title) return { metadata: null, name: null }
discoveryRequestPath = `v1/tracks?handle=${handle}&slug=${title}`
break
}
case 'playlist': {
const { handle, title } = route.params
if (!handle || !title) return { metadata: null, name: null }
discoveryRequestPath = `v1/resolve?url=${pathname}`
// TODO: Uncomment when by_permalink routes are working properly
// discoveryRequestPath = `v1/full/playlists/by_permalink/${handle}/${title}`
break
}
case 'album': {
const { handle, title } = route.params
if (!handle || !title) return { metadata: null, name: null }
discoveryRequestPath = `v1/resolve?url=${pathname}`
// TODO: Uncomment when by_permalink routes are working properly
// discoveryRequestPath = `v1/full/playlists/by_permalink/${handle}/${title}`
break
}
default:
return { metadata: null, name: null }
}
try {
const res = await fetch(`${discoveryNode}/${discoveryRequestPath}`)
if (res.status !== 200) {
throw new Error(res.status)
}
const json = await res.json()
return { metadata: json, name: route.name }
} catch (e) {
return { metadata: null, name: null }
}
}

function clean(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}

class HeadElementHandler {
constructor(pathname) {
self.pathname = pathname
}

async element(element) {
const { metadata, name } = await getMetadata(self.pathname)

if (!metadata || !name) {
// We did parse this to anything we have custom tags for, so just return the default tags
raymondjacobson marked this conversation as resolved.
Show resolved Hide resolved
const tags = `<meta property="og:title" content="Audius - Empowering Creators">
<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">
<meta property="og:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
<meta property="og:image" content="https://audius.co/ogImage.jpg">
<meta name="twitter:title" content="Audius - Empowering Creators">
<meta name="twitter:description" content="Audius is a music streaming and sharing platform that puts power back into the hands of content creators.">
<meta name="twitter:image" content="https://audius.co/ogImage.jpg">
<meta name="twitter:image:alt" content="The Audius Platform">`
element.append(tags, { html: true })
return
}

let title, description, ogDescription, image, permalink
switch (name) {
case 'user': {
title = `Stream ${metadata.data.name} on Audius`
description = `Play ${metadata.data.name} on Audius | Listen to tracks, albums, playlists on desktop and mobile on Audius.`
ogDescription = metadata.data.bio || description
image = metadata.data.profile_picture
? metadata.data.profile_picture['1000x1000']
: ''
permalink = `/${metadata.data.handle}`
break
}
case 'track': {
description = `Stream ${metadata.data.title} by ${metadata.data.user.name} on Audius. Listen on desktop and mobile.`
title = `${metadata.data.title} | Stream ${metadata.data.user.name}`
ogDescription = metadata.data.description || description
image = metadata.data.artwork ? metadata.data.artwork['1000x1000'] : ''
permalink = metadata.data.permalink
break
}
case 'playlist': {
description = `Listen to ${metadata.data[0].playlist_name}, a playlist curated by ${metadata.data[0].user.name} on desktop and mobile.`
title = `${metadata.data[0].playlist_name} | Playlist by ${metadata.data[0].user.name}`
ogDescription = metadata.data[0].description || ''
image = metadata.data[0].artwork
? metadata.data[0].artwork['1000x1000']
: ''
permalink = metadata.data[0].permalink
break
}
case 'album': {
description = `Listen to ${metadata.data[0].playlist_name}, a playlist curated by ${metadata.data[0].user.name} on desktop and mobile.`
title = `${metadata.data[0].playlist_name} | Playlist by ${metadata.data[0].user.name}`
ogDescription = metadata.data[0].description || ''
image = metadata.data[0].artwork
? metadata.data[0].artwork['1000x1000']
: ''
permalink = metadata.data[0].permalink
break
}
default:
return
}
const tags = `<title>${clean(title)}</title>
<meta name="description" content="${clean(description)}">

<link rel="canonical" href="https://audius.co${permalink}">

<meta property="og:title" content="${clean(title)}">
<meta property="og:description" content="${clean(ogDescription)}">
<meta property="og:image" content="${image}">
<meta property="og:url" content="https://audius.co${permalink}">

<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="${clean(title)}">
<meta name="twitter:description" content="${clean(ogDescription)}">
<meta name="twitter:image" content=https://audius.co${permalink}">`
element.append(tags, { html: true })
}
}

async function handleEvent(event) {
const url = new URL(event.request.url)
const { pathname, search, hash } = url
Expand Down Expand Up @@ -82,7 +265,12 @@ async function handleEvent(event) {
}
}

return await getAssetFromKV(event, options)
const asset = await getAssetFromKV(event, options)

const rewritten = new HTMLRewriter()
.on('head', new HeadElementHandler(pathname))
.transform(asset)
return rewritten
} catch (e) {
return new Response(e.message || e.toString(), { status: 500 })
}
Expand Down
6 changes: 6 additions & 0 deletions packages/web/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@ vars = { ENVIRONMENT = "production", GA = "https://general-admission.audius.co",

[env.production]
name = "audius"
vars = { ENVIRONMENT = "production", GA = "https://general-admission.audius.co", SITEMAP = "http://audius.co.s3-website-us-west-1.amazonaws.com" }

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