diff --git a/packages/web/src/pages/unavailable-track-page/ServerUnavailableTrack.tsx b/packages/web/src/pages/not-found-page/ServerNotFound.tsx similarity index 69% rename from packages/web/src/pages/unavailable-track-page/ServerUnavailableTrack.tsx rename to packages/web/src/pages/not-found-page/ServerNotFound.tsx index 3326d5b0372..16696dfa003 100644 --- a/packages/web/src/pages/unavailable-track-page/ServerUnavailableTrack.tsx +++ b/packages/web/src/pages/not-found-page/ServerNotFound.tsx @@ -5,17 +5,20 @@ import { Text } from '@audius/harmony/src/components/text' import { Link } from 'react-router' const messages = { - heading: 'This Track Isn’t Available', - description: 'This track can no longer be streamed on Audius.', + heading: 'Whoops', + description: 'This is not the page you’re looking for...', buttonText: 'Take Me Back To The Music' } /** - * Server-rendered twin of the unavailable-track message. Kept separate, and on - * deep harmony imports, so the SSR worker bundle doesn't pull in the client - * barrels - matching the other Server* page components. + * Server-rendered twin of NotFoundPage. Kept separate, and on deep harmony + * imports, so the SSR worker bundle doesn't pull in the client barrels - + * matching the other Server* page components. The Lottie animation and tiled + * background from the client page are deliberately left out: they'd pull a + * large animation payload into the worker bundle for a page the client + * replaces on hydration anyway. */ -export const ServerUnavailableTrack = () => { +export const ServerNotFound = () => { return ( { !!currentTrack && currentTrack.track_id === track.track_id - // Simple error handling + // Simple error handling. A track whose owner is no longer active - a self + // deactivation, or an account delisted by the trusted notifier - is treated + // the same as one that doesn't exist: the API already 404s its stream so it + // can't be told apart from a missing track, and the page shouldn't either. useEffect(() => { - if (status === 'error') { + if (status === 'error' || isTrackUnavailable(track)) { navigate(NOT_FOUND_PAGE) } - }, [status, navigate]) + }, [status, track, navigate]) // Simple cleanup useEffect(() => { @@ -263,11 +265,11 @@ const TrackPage = () => { ) } - // The API reports tracks whose owner is no longer active as non-streamable. - // Honor that instead of rendering a playable track page. Checked after the - // deleted case above, which has its own more specific treatment. + // The redirect above runs in an effect, which fires after the first paint. + // Render nothing until it lands so the track's title and artwork never reach + // the screen, even for a frame. if (isTrackUnavailable(track)) { - return + return null } const renderGiantTrackTile = () => ( diff --git a/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx b/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx index 4cae89e986e..f812191e51d 100644 --- a/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx +++ b/packages/web/src/pages/track-page/components/mobile/TrackPage.tsx @@ -47,7 +47,6 @@ import NavContext, { } from 'components/nav/mobile/NavContext' import DeletedPage from 'pages/deleted-page/DeletedPage' import { getTrackDefaults } from 'pages/track-page/utils' -import { UnavailableTrackPage } from 'pages/unavailable-track-page/UnavailableTrackPage' import { getTrackPageContext } from 'ssr/metaTags' import { parseTrackRoute } from 'utils/route/trackRouteParser' @@ -85,12 +84,15 @@ const TrackPage = () => { !!currentTrack && currentTrack.track_id === track.track_id - // Simple error handling + // Simple error handling. A track whose owner is no longer active - a self + // deactivation, or an account delisted by the trusted notifier - is treated + // the same as one that doesn't exist: the API already 404s its stream so it + // can't be told apart from a missing track, and the page shouldn't either. useEffect(() => { - if (status === 'error') { + if (status === 'error' || isTrackUnavailable(track)) { navigate(NOT_FOUND_PAGE) } - }, [status, navigate]) + }, [status, track, navigate]) const { setLeft, setCenter, setRight } = useContext(NavContext)! useEffect(() => { setLeft(LeftPreset.BACK) @@ -277,11 +279,11 @@ const TrackPage = () => { ) } - // The API reports tracks whose owner is no longer active as non-streamable. - // Honor that instead of rendering a playable track page. Checked after the - // deleted case above, which has its own more specific treatment. + // The redirect above runs in an effect, which fires after the first paint. + // Render nothing until it lands so the track's title and artwork never reach + // the screen, even for a frame. if (isTrackUnavailable(track)) { - return + return null } return ( diff --git a/packages/web/src/pages/unavailable-track-page/UnavailableTrackPage.module.css b/packages/web/src/pages/unavailable-track-page/UnavailableTrackPage.module.css deleted file mode 100644 index 27044c2cea9..00000000000 --- a/packages/web/src/pages/unavailable-track-page/UnavailableTrackPage.module.css +++ /dev/null @@ -1,9 +0,0 @@ -/** - * The message is the only content on the page, so stretch the container to the - * full scroll area and let the message's `flex: 1` center it rather than - * pinning it to the top of the viewport. - */ -.container { - flex-direction: column; - min-height: 100%; -} diff --git a/packages/web/src/pages/unavailable-track-page/UnavailableTrackPage.tsx b/packages/web/src/pages/unavailable-track-page/UnavailableTrackPage.tsx deleted file mode 100644 index 00bef97aea8..00000000000 --- a/packages/web/src/pages/unavailable-track-page/UnavailableTrackPage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { route } from '@audius/common/utils' -import { Button, Flex, IconArrowRight, Text } from '@audius/harmony' -import { Link } from 'react-router' - -import MobilePageContainer from 'components/mobile-page-container/MobilePageContainer' -import Page from 'components/page/Page' -import { useIsMobile } from 'hooks/useIsMobile' - -import styles from './UnavailableTrackPage.module.css' - -const { HOME_PAGE } = route - -const messages = { - title: 'Track Unavailable', - heading: 'This Track Isn’t Available', - description: 'This track can no longer be streamed on Audius.', - buttonText: 'Take Me Back To The Music' -} - -const UnavailableTrackContent = ({ isMobile }: { isMobile: boolean }) => ( - - - - - {messages.heading} - - - {messages.description} - - - - - -) - -/** - * Shown in place of a track page the API reports as non-streamable - today - * that means the owner is no longer active. Deliberately says nothing about - * the account, since the same flag covers an artist deactivating their own - * account and an account being delisted, and emits none of the track's own - * metadata or artwork. Marked noIndex so it stays out of search results. - */ -export const UnavailableTrackPage = () => { - const isMobile = useIsMobile() - - if (isMobile) { - return ( - - - - ) - } - - return ( - - - - ) -} - -export default UnavailableTrackPage diff --git a/packages/web/src/ssr/metaTags.ts b/packages/web/src/ssr/metaTags.ts index cd760a7b551..9c0cc0761d9 100644 --- a/packages/web/src/ssr/metaTags.ts +++ b/packages/web/src/ssr/metaTags.ts @@ -163,16 +163,17 @@ export const getDefaultContext = () => { } /** - * Meta tag context for a track the API reports as non-streamable (its owner is - * no longer active). Deliberately generic - none of the track's or the - * account's title, artwork, or canonical URL - and always paired with - * `noIndex` at the call site so these pages stay out of search results and - * social unfurls. - */ -export const getUnavailableTrackContext = () => ({ - title: 'Track Unavailable', - description: 'This track can no longer be streamed on Audius.', - ogDescription: 'This track can no longer be streamed on Audius.', + * Meta tag context for a page served as a 404. Used for tracks the API reports + * as non-streamable (their owner is no longer active), which are rendered as + * plain not-found pages so they can't be told apart from a track that never + * existed. Carries none of the track's or the account's title, artwork, or + * canonical URL, and is always paired with `noIndex` at the call site so these + * pages stay out of search results and social unfurls. + */ +export const getNotFoundContext = () => ({ + title: 'Not Found', + description: '404 - Page not found', + ogDescription: '404 - Page not found', image: DEFAULT_IMAGE_URL, imageAlt: 'The Audius Platform', thumbnail: true diff --git a/packages/web/src/ssr/track/+onRenderHtml.tsx b/packages/web/src/ssr/track/+onRenderHtml.tsx index e1502221d4a..aaef70f6bdc 100644 --- a/packages/web/src/ssr/track/+onRenderHtml.tsx +++ b/packages/web/src/ssr/track/+onRenderHtml.tsx @@ -9,16 +9,16 @@ import type { PageContextServer } from 'vike/types' import { ServerWebPlayer } from 'app/web-player/ServerWebPlayer' import { MetaTags } from 'components/meta-tags/MetaTags' +import { ServerNotFound } from 'pages/not-found-page/ServerNotFound' import { DesktopServerTrackPage } from 'pages/track-page/DesktopServerTrackPage' import { MobileServerTrackPage } from 'pages/track-page/MobileServerTrackPage' -import { ServerUnavailableTrack } from 'pages/unavailable-track-page/ServerUnavailableTrack' import { canEmbed, DEFAULT_IMAGE_URL, getAppUrl, getEmbedUrl, getTrackPageContext, - getUnavailableTrackContext, + getNotFoundContext, getWebUrl, isDiscord } from 'ssr/metaTags' @@ -53,8 +53,10 @@ export default function render(pageContext: TrackPageContext) { const isMobile = isMobileUserAgent(userAgent) // The API reports tracks whose owner is no longer active as non-streamable. - // Serve none of their metadata: no title, artwork, embed player, or index. - // Explicit `=== false` because the field is absent on older API responses. + // Serve none of their metadata: no title, artwork, embed player, or index - + // the page is rendered as a plain 404, matching what the client navigates to + // and what the API returns for these tracks. Explicit `=== false` because the + // field is absent on older API responses. const isUnavailable = track?.is_streamable === false // Check if this request can show an embed player (Twitter/Discord bots) @@ -71,7 +73,7 @@ export default function render(pageContext: TrackPageContext) { // Build meta tags - use comment-specific if we have comment data let seoMetadata if (isUnavailable) { - seoMetadata = getUnavailableTrackContext() + seoMetadata = getNotFoundContext() } else if (commentData) { const trackName = commentData.track.title const artistName = commentData.track.user.name @@ -135,7 +137,7 @@ export default function render(pageContext: TrackPageContext) { noIndex={isUnavailable} /> {isUnavailable ? ( - + ) : isMobile ? ( ) : (