Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
85 changes: 83 additions & 2 deletions packages/web/src/components/meta-tags/MetaTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ export type MetaTagsProps = {
* Hash ID for OG URL generation (id field from entities)
*/
hashId?: string
/**
* Whether to show embed player (for Twitter/Discord)
*/
embed?: boolean
/**
* URL to the embed player (e.g. /embed/track/xyz?flavor=card&twitter=true)
*/
embedUrl?: string
/**
* Deep link URL for mobile apps (e.g. audius://track/xyz)
*/
appUrl?: string
/**
* Web URL for the current page (e.g. https://audius.co/track/xyz)
*/
webUrl?: string
/**
* Whether the image shows as a small thumbnail version.
* Controls twitter:card type (summary vs summary_large_image)
*/
thumbnail?: boolean
}

/**
Expand Down Expand Up @@ -70,7 +91,12 @@ export const MetaTags = (props: MetaTagsProps) => {
structuredData,
noIndex = false,
entityType,
hashId
hashId,
embed = false,
embedUrl,
appUrl,
webUrl,
thumbnail = true
} = props

const formattedTitle = title
Expand All @@ -80,6 +106,13 @@ export const MetaTags = (props: MetaTagsProps) => {
// Generate OG URL if entity type and hash ID are provided
const ogUrl = generateOgUrl(entityType, hashId)

// Determine twitter card type based on thumbnail and embed settings
const getTwitterCardType = () => {
if (thumbnail) return 'summary'
if (embed && embedUrl) return 'player'
return 'summary_large_image'
}

return (
<>
{/* noIndex */}
Expand Down Expand Up @@ -124,6 +157,12 @@ export const MetaTags = (props: MetaTagsProps) => {
<Helmet encodeSpecialCharacters={false}>
<meta property='og:image' content={image} />
<meta name='twitter:image' content={image} />
{thumbnail ? null : (
<>
<meta property='og:image:width' content='1000' />
<meta property='og:image:height' content='1000' />
</>
)}
</Helmet>
) : null}

Expand All @@ -134,11 +173,53 @@ export const MetaTags = (props: MetaTagsProps) => {
</Helmet>
) : null}

{/* OG Type and Twitter Card */}
<Helmet encodeSpecialCharacters={false}>
<meta property='og:type' content='website' />
<meta name='twitter:card' content='summary' />
<meta name='twitter:card' content={getTwitterCardType()} />
</Helmet>

{/* Twitter Player (for embeds) */}
{embed && embedUrl ? (
<Helmet encodeSpecialCharacters={false}>
<meta name='twitter:player' content={embedUrl} />
<meta name='twitter:player:width' content='480' />
<meta name='twitter:player:height' content='480' />
</Helmet>
) : null}

{/* Twitter App Links */}
{appUrl ? (
<Helmet encodeSpecialCharacters={false}>
<meta name='twitter:app:name:iphone' content='Audius Music' />
<meta name='twitter:app:id:iphone' content='1491270519' />
<meta name='twitter:app:url:iphone' content={appUrl} />
<meta name='twitter:app:name:ipad' content='Audius Music' />
<meta name='twitter:app:id:ipad' content='1491270519' />
<meta name='twitter:app:url:ipad' content={appUrl} />
<meta name='twitter:app:name:googleplay' content='Audius Music' />
<meta name='twitter:app:id:googleplay' content='co.audius.app' />
<meta name='twitter:app:url:googleplay' content={appUrl} />
</Helmet>
) : null}

{/* Farcaster Frame */}
{webUrl ? (
<Helmet encodeSpecialCharacters={false}>
<meta property='fc:frame' content='vNext' />
<meta property='fc:frame:image:aspect_ratio' content='1:1' />
<meta property='fc:frame:button:1' content='Listen on Audius!' />
<meta property='fc:frame:button:1:action' content='link' />
<meta property='fc:frame:button:1:target' content={webUrl} />
</Helmet>
) : null}

{webUrl && image ? (
<Helmet encodeSpecialCharacters={false}>
<meta property='fc:frame:image' content={image} />
</Helmet>
) : null}

{structuredData ? (
<Helmet encodeSpecialCharacters={false}>
<script type='application/ld+json'>
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/ssr/audio/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Empty page, everything is handled in +onRenderHtml
export default function render() {
return null
}

41 changes: 41 additions & 0 deletions packages/web/src/ssr/audio/+onRenderHtml.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// $AUDIO token page SSR - meta tags only

import { renderToString } from 'react-dom/server'
import { Helmet } from 'react-helmet'
import { escapeInject, dangerouslySkipEscape } from 'vike/server'

import { MetaTags } from 'components/meta-tags/MetaTags'
import { getIndexHtml } from 'ssr/getIndexHtml'
import { getAudioContext } from 'ssr/metaTags'

export function render() {
const context = getAudioContext()

const pageHtml = renderToString(
<>
<MetaTags
title={context.title}
description={context.description}
image={context.image}
thumbnail={context.thumbnail}
/>
<div />
</>
)

const helmet = Helmet.renderStatic()

const html = getIndexHtml()
.replace(`<div id="root"></div>`, `<div id="root">${pageHtml}</div>`)
.replace(
`<meta property="helmet" />`,
`
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
`
)

return escapeInject`${dangerouslySkipEscape(html)}`
}

4 changes: 4 additions & 0 deletions packages/web/src/ssr/audio/+route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { makePageRoute } from 'ssr/util'

export default makePageRoute(['/audio'], 'Audio Token Page')

19 changes: 18 additions & 1 deletion packages/web/src/ssr/collection/+onRenderHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ServerWebPlayer } from 'app/web-player/ServerWebPlayer'
import { MetaTags } from 'components/meta-tags/MetaTags'
import { DesktopServerCollectionPage } from 'pages/collection-page/DesktopServerCollectionPage'
import { MobileServerCollectionPage } from 'pages/collection-page/MobileServerCollectionPage'
import { canEmbed, getAppUrl, getEmbedUrl, getWebUrl } from 'ssr/metaTags'
import { isMobileUserAgent } from 'utils/clientUtil'
import { getCollectionPageSEOFields } from 'utils/seo'

Expand All @@ -32,6 +33,9 @@ export default function render(pageContext: CollectionPageContext) {
const userAgent = headers?.['user-agent'] ?? ''
const isMobile = isMobileUserAgent(userAgent)

// Check if this request can show an embed player (Twitter/Discord bots)
const shouldEmbed = canEmbed(userAgent)

// Create a fresh cache instance for this SSR request
// This ensures the theme context is properly connected
const cache = createCache({ key: 'harmony', prepend: true })
Expand All @@ -48,11 +52,24 @@ export default function render(pageContext: CollectionPageContext) {
hashId: id
})

// Generate embed and app URLs
const embedType = is_album ? 'album' : 'playlist'
const embedUrl = getEmbedUrl(embedType, id)
const appUrl = getAppUrl(urlPathname)
const webUrl = getWebUrl(urlPathname)

const pageHtml = renderToString(
<CacheProvider value={cache}>
<ServerWebPlayer isMobile={isMobile} location={urlPathname}>
<>
<MetaTags {...seoMetadata} />
<MetaTags
{...seoMetadata}
embed={shouldEmbed}
embedUrl={embedUrl}
appUrl={appUrl}
webUrl={webUrl}
thumbnail={false}
/>
{isMobile ? (
<MobileServerCollectionPage collection={collection} user={user} />
) : (
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/ssr/download/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Empty page, everything is handled in +onRenderHtml
export default function render() {
return null
}

41 changes: 41 additions & 0 deletions packages/web/src/ssr/download/+onRenderHtml.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Download app page SSR - meta tags only

import { renderToString } from 'react-dom/server'
import { Helmet } from 'react-helmet'
import { escapeInject, dangerouslySkipEscape } from 'vike/server'

import { MetaTags } from 'components/meta-tags/MetaTags'
import { getIndexHtml } from 'ssr/getIndexHtml'
import { getDownloadAppContext } from 'ssr/metaTags'

export function render() {
const context = getDownloadAppContext()

const pageHtml = renderToString(
<>
<MetaTags
title={context.title}
description={context.description}
image={context.image}
thumbnail={context.thumbnail}
/>
<div />
</>
)

const helmet = Helmet.renderStatic()

const html = getIndexHtml()
.replace(`<div id="root"></div>`, `<div id="root">${pageHtml}</div>`)
.replace(
`<meta property="helmet" />`,
`
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
`
)

return escapeInject`${dangerouslySkipEscape(html)}`
}

4 changes: 4 additions & 0 deletions packages/web/src/ssr/download/+route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { makePageRoute } from 'ssr/util'

export default makePageRoute(['/download'], 'Download Page')

5 changes: 5 additions & 0 deletions packages/web/src/ssr/explore/+Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Empty page, everything is handled in +onRenderHtml
export default function render() {
return null
}

56 changes: 56 additions & 0 deletions packages/web/src/ssr/explore/+onRenderHtml.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Explore page SSR - meta tags only

import { renderToString } from 'react-dom/server'
import { Helmet } from 'react-helmet'
import { escapeInject, dangerouslySkipEscape } from 'vike/server'
import type { PageContextServer } from 'vike/types'

import { MetaTags } from 'components/meta-tags/MetaTags'
import { getIndexHtml } from 'ssr/getIndexHtml'
import { getExploreInfo } from 'ssr/metaTags'

type ExplorePageContext = PageContextServer & {
routeParams: {
type?: string
}
}

export function render(pageContext: ExplorePageContext) {
const { routeParams, urlPathname } = pageContext

// Handle /trending/playlists as a special case
let type = routeParams.type
if (urlPathname === '/trending/playlists') {
type = 'trending-playlists'
}

const context = getExploreInfo(type)

const pageHtml = renderToString(
<>
<MetaTags
title={context.title}
description={context.description}
image={context.image}
thumbnail
/>
<div />
</>
)

const helmet = Helmet.renderStatic()

const html = getIndexHtml()
.replace(`<div id="root"></div>`, `<div id="root">${pageHtml}</div>`)
.replace(
`<meta property="helmet" />`,
`
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
`
)

return escapeInject`${dangerouslySkipEscape(html)}`
}

7 changes: 7 additions & 0 deletions packages/web/src/ssr/explore/+route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { makePageRoute } from 'ssr/util'

export default makePageRoute(
['/explore', '/explore/@type', '/trending/playlists'],
'Explore Page'
)

Loading