Share songs, with link previews
CI / check (push) Canceled after 0s

- "Share ↗" on every song (web and mobile): the phone's share sheet, or
  copy the link on desktop. Links go to a new song page, /t/<id>?from=you,
  with big artwork and open-in buttons for every service.
- Server-rendered previews (Open Graph / Twitter cards) for song pages and
  profiles: title, who shared it or what they're playing, and the album
  art, avatar or logo. The web server fetches from the API over the
  compose network (INTERNAL_API_URL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-06 13:31:47 -04:00
co-authored by Claude Opus 5.5
parent abe1a24bb4
commit edc0c7b999
13 changed files with 332 additions and 144 deletions
+8 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { parseProfileLink, profileUrl } from './links';
import { parseProfileLink, profileUrl, trackUrl } from './links';
describe('profileUrl', () => {
it('builds a website profile link, tolerating a trailing slash', () => {
@@ -34,3 +34,10 @@ describe('parseProfileLink', () => {
expect(parseProfileLink(text)).toBeNull();
});
});
describe('trackUrl', () => {
it('links to the song page, optionally saying who shared it', () => {
expect(trackUrl('https://m.test/', 'abc')).toBe('https://m.test/t/abc');
expect(trackUrl('https://m.test', 'abc', 'alice')).toBe('https://m.test/t/abc?from=alice');
});
});
+9
View File
@@ -29,6 +29,15 @@ export function parseProfileLink(text: string): string | null {
return match ? valid(decodeURIComponent(match[1]!)) : null;
}
/** Shareable song page on the website, e.g. https://musicshare.app/t/<id>?from=alice */
export function trackUrl(webBase: string, trackId: string, from?: string | null): string {
const url = `${webBase.replace(/\/+$/, '')}/t/${encodeURIComponent(trackId)}`;
return from ? `${url}?from=${encodeURIComponent(from)}` : url;
}
/** What goes in the share sheet next to the link. */
export const shareText = (track: { title: string; artist: string }) => `${track.title} by ${track.artist}`;
function valid(name: string): string | null {
const username = name.toLowerCase();
return USERNAME.test(username) ? username : null;