feat: add Open Graph and Twitter Card meta tags for public pages

- Introduce appBaseUrl config option for absolute URLs in social cards
- Generate og metadata for song and playlist pages with title, description, image, and URL
- Include partial layout support for rendering OG/Twitter meta tags conditionally
This commit is contained in:
Brian Fertig 2026-04-12 10:43:19 -06:00
parent 20bd6422a6
commit 347cf97da7
3 changed files with 28 additions and 3 deletions

View File

@ -56,6 +56,7 @@ const { csrfSynchronisedProtection, generateToken } = csrfSync({
// Attach helpers for templates. // Attach helpers for templates.
app.use((req, res, next) => { app.use((req, res, next) => {
res.locals.siteName = process.env.SITE_NAME || 'Bri-Tunes'; res.locals.siteName = process.env.SITE_NAME || 'Bri-Tunes';
res.locals.appBaseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
res.locals.user = req.session.user || null; res.locals.user = req.session.user || null;
res.locals.flash = req.session.flash || null; res.locals.flash = req.session.flash || null;
delete req.session.flash; delete req.session.flash;

View File

@ -78,7 +78,13 @@ router.get('/songs/:slug', (req, res, next) => {
const userId = req.session.user?.id || null; const userId = req.session.user?.id || null;
social.enrichSongs([song], userId); social.enrichSongs([song], userId);
const creator = song.uploadedBy ? users.findById(song.uploadedBy) : null; const creator = song.uploadedBy ? users.findById(song.uploadedBy) : null;
res.render('public/song', { title: song.title, song, creator }); const og = {
title: `${res.locals.siteName}: ${song.title} by ${song.artist}`,
description: `Check out ${song.artist}'s hit ${song.title}, available on ${res.locals.siteName}`,
image: song.coverPath ? `${res.locals.appBaseUrl}/media/${song.coverPath}` : null,
url: `${res.locals.appBaseUrl}/songs/${song.slug}`,
};
res.render('public/song', { title: song.title, song, creator, og });
}); });
router.get('/playlists', (req, res) => { router.get('/playlists', (req, res) => {
@ -101,7 +107,13 @@ router.get('/playlists/:slug', (req, res, next) => {
const tracks = playlists.getTracks(playlist.id).filter(t => canView(t.visibility, t.uploadedBy, userId)); const tracks = playlists.getTracks(playlist.id).filter(t => canView(t.visibility, t.uploadedBy, userId));
social.enrichSongs(tracks, userId); social.enrichSongs(tracks, userId);
const creator = playlist.createdBy ? users.findById(playlist.createdBy) : null; const creator = playlist.createdBy ? users.findById(playlist.createdBy) : null;
res.render('public/playlist', { title: playlist.title, playlist, tracks, creator }); const og = {
title: `${res.locals.siteName} Playlist: ${playlist.title}`,
description: playlist.description || null,
image: playlist.coverPath ? `${res.locals.appBaseUrl}/media/${playlist.coverPath}` : null,
url: `${res.locals.appBaseUrl}/playlists/${playlist.slug}`,
};
res.render('public/playlist', { title: playlist.title, playlist, tracks, creator, og });
}); });
router.get('/profiles', (req, res) => { router.get('/profiles', (req, res) => {

View File

@ -5,6 +5,18 @@
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="csrf-token" content="<%= csrfToken %>"> <meta name="csrf-token" content="<%= csrfToken %>">
<title><%= typeof title !== 'undefined' ? title : siteName %></title> <title><%= typeof title !== 'undefined' ? title : siteName %></title>
<% if (typeof og !== 'undefined' && og) { %>
<meta property="og:type" content="website">
<meta property="og:site_name" content="<%= siteName %>">
<meta property="og:title" content="<%= og.title %>">
<meta property="og:url" content="<%= og.url %>">
<% if (og.description) { %><meta property="og:description" content="<%= og.description %>"><% } %>
<% if (og.image) { %><meta property="og:image" content="<%= og.image %>"><% } %>
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<%= og.title %>">
<% if (og.description) { %><meta name="twitter:description" content="<%= og.description %>"><% } %>
<% if (og.image) { %><meta name="twitter:image" content="<%= og.image %>"><% } %>
<% } %>
<link rel="preload" href="/static/vendor/fonts/Orbitron.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/static/vendor/fonts/Orbitron.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/static/vendor/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/static/vendor/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="icon" href="/static/favicon.ico" sizes="any"> <link rel="icon" href="/static/favicon.ico" sizes="any">