diff options
-rw-r--r-- | src/page/track.js | 38 | ||||
-rw-r--r-- | src/strings-default.json | 8 | ||||
-rwxr-xr-x | src/upd8.js | 89 | ||||
-rw-r--r-- | src/util/html.js | 19 |
4 files changed, 141 insertions, 13 deletions
diff --git a/src/page/track.js b/src/page/track.js index 5249ea44..3295d48a 100644 --- a/src/page/track.js +++ b/src/page/track.js @@ -116,10 +116,36 @@ export function write(track, {wikiData}) { }) }; + const getSocialEmbedDescription = ({ + getArtistString: _getArtistString, + language, + }) => { + const hasArtists = (track.artistContribs?.length > 0); + const hasCoverArtists = (track.coverArtistContribs?.length > 0); + const getArtistString = contribs => _getArtistString(contribs, { + // We don't want to put actual HTML tags in social embeds (sadly + // they don't get parsed and displayed, generally speaking), so + // override the link argument so that artist "links" just show + // their names. + link: {artist: artist => artist.name} + }); + if (!hasArtists && !hasCoverArtists) return ''; + return language.formatString( + 'trackPage.socialEmbed.body' + [ + hasArtists && '.withArtists', + hasCoverArtists && '.withCoverArtists', + ].filter(Boolean).join(''), + Object.fromEntries([ + hasArtists && ['artists', getArtistString(track.artistContribs)], + hasCoverArtists && ['coverArtists', getArtistString(track.coverArtistContribs)], + ].filter(Boolean))) + }; + const page = { type: 'page', path: ['track', track.directory], page: ({ + absoluteTo, fancifyURL, generateChronologyLinks, generateCoverLink, @@ -134,7 +160,8 @@ export function write(track, {wikiData}) { transformInline, transformLyrics, transformMultiline, - to + to, + urls, }) => { const generateTrackList = bindOpts(unbound_generateTrackList, {getArtistString, link, language}); const cover = getTrackCover(track); @@ -147,6 +174,15 @@ export function write(track, {wikiData}) { `--track-directory: ${track.directory}` ]), + socialEmbed: { + heading: language.$('trackPage.socialEmbed.heading', {album: track.album.name}), + headingLink: absoluteTo('localized.album', album.directory), + title: language.$('trackPage.socialEmbed.title', {track: track.name}), + description: getSocialEmbedDescription({getArtistString, language}), + image: '/' + getTrackCover(track, {to: urls.from('shared.root').to}), + color: track.color, + }, + // disabled for now! shifting banner position per height of page is disorienting /* banner: album.bannerArtistContribs.length && { diff --git a/src/strings-default.json b/src/strings-default.json index cc4ec1d4..f1a53027 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -156,6 +156,7 @@ "misc.skippers.skipToSidebar.left": "Skip to sidebar (left)", "misc.skippers.skipToSidebar.right": "Skip to sidebar (right)", "misc.skippers.skipToFooter": "Skip to footer", + "misc.socialEmbed.heading": "{WIKI_NAME} | {HEADING}", "misc.jumpTo": "Jump to:", "misc.jumpTo.withLinks": "Jump to: {LINKS}.", "misc.contentWarnings": "cw: {WARNINGS}", @@ -358,5 +359,10 @@ "trackPage.referenceList.official": "Official:", "trackPage.nav.track": "{TRACK}", "trackPage.nav.track.withNumber": "{NUMBER}. {TRACK}", - "trackPage.nav.random": "Random" + "trackPage.nav.random": "Random", + "trackPage.socialEmbed.heading": "{ALBUM}", + "trackPage.socialEmbed.title": "{TRACK}", + "trackPage.socialEmbed.body.withArtists.withCoverArtists": "By {ARTISTS}; art by {COVER_ARTISTS}.", + "trackPage.socialEmbed.body.withArtists": "By {ARTISTS}.", + "trackPage.socialEmbed.body.withCoverArtists": "Art by {COVER_ARTISTS}." } diff --git a/src/upd8.js b/src/upd8.js index 12f1af3a..b999ef7f 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -194,6 +194,9 @@ const UTILITY_DIRECTORY = 'util'; // (This gets symlinked into the --data-path directory.) const STATIC_DIRECTORY = 'static'; +// This exists adjacent to index.html for any page with oEmbed metadata. +const OEMBED_JSON_FILE = 'oembed.json'; + function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); } @@ -822,12 +825,13 @@ writePage.to = ({ return path; }; -writePage.html = (pageFn, { +writePage.html = (pageInfo, { defaultLanguage, language, languages, localizedPaths, paths, + oEmbedJSONHref, to, transformMultiline, wikiData @@ -847,8 +851,9 @@ writePage.html = (pageFn, { sidebarLeft = {}, sidebarRight = {}, nav = {}, - footer = {} - } = pageFn({to}); + footer = {}, + socialEmbed = {}, + } = pageInfo; body.style ??= ''; @@ -1059,6 +1064,14 @@ writePage.html = (pageFn, { </div> `; + const socialEmbedHTML = [ + socialEmbed.title && html.tag('meta', {property: 'og:title', content: socialEmbed.title}), + socialEmbed.description && html.tag('meta', {property: 'og:description', content: socialEmbed.description}), + socialEmbed.image && html.tag('meta', {property: 'og:image', content: socialEmbed.image}), + socialEmbed.color && html.tag('meta', {name: 'theme-color', content: socialEmbed.color}), + oEmbedJSONHref && html.tag('link', {type: 'application/json+oembed', href: oEmbedJSONHref}), + ].filter(Boolean).join('\n'); + return filterEmptyLines(fixWS` <!DOCTYPE html> <html ${html.attributes({ @@ -1075,6 +1088,7 @@ writePage.html = (pageFn, { ${Object.entries(meta).filter(([ key, value ]) => value).map(([ key, value ]) => `<meta ${key}="${html.escapeAttributeValue(value)}">`).join('\n')} ${canonical && `<link rel="canonical" href="${canonical}">`} ${localizedCanonical.map(({ lang, href }) => `<link rel="alternate" hreflang="${lang}" href="${href}">`).join('\n')} + ${socialEmbedHTML} <link rel="stylesheet" href="${to('shared.staticFile', `site.css?${CACHEBUST}`)}"> ${(theme || stylesheet) && fixWS` <style> @@ -1111,9 +1125,40 @@ writePage.html = (pageFn, { `); }; -writePage.write = async (content, {paths}) => { +writePage.oEmbedJSON = (pageInfo, { + language, + wikiData, +}) => { + const { socialEmbed } = pageInfo; + const { wikiInfo } = wikiData; + const { canonicalBase, nameShort } = wikiInfo; + + const entries = [ + socialEmbed.heading && ['author_name', + language.$('misc.socialEmbed.heading', { + wikiName: nameShort, + heading: socialEmbed.heading + })], + socialEmbed.headingLink && canonicalBase && ['author_url', + canonicalBase.replace(/\/$/, '') + '/' + + socialEmbed.headingLink.replace(/^\//, '')], + ].filter(Boolean); + + if (!entries.length) return ''; + + return JSON.stringify(Object.fromEntries(entries)); +}; + +writePage.write = async ({ + html, + oEmbedJSON = '', + paths, +}) => { await mkdir(paths.outputDirectory, {recursive: true}); - await writeFile(paths.outputFile, content); + await Promise.all([ + writeFile(paths.outputFile, html), + oEmbedJSON && writeFile(paths.oEmbedJSONFile, oEmbedJSON) + ].filter(Boolean)); }; // TODO: This only supports one <>-style argument. @@ -1132,12 +1177,14 @@ writePage.paths = (baseDirectory, fullKey, directory = '', { const outputDirectory = path.join(outputPath, pathname); const outputFile = path.join(outputDirectory, file); + const oEmbedJSONFile = path.join(outputDirectory, OEMBED_JSON_FILE); return { toPath: [fullKey, directory], pathname, subdirectoryPrefix, - outputDirectory, outputFile + outputDirectory, outputFile, + oEmbedJSONFile, }; }; @@ -1909,6 +1956,14 @@ async function main() { paths }); + const absoluteTo = (targetFullKey, ...args) => { + const [ groupKey, subKey ] = targetFullKey.split('.'); + const from = urls.from('shared.root'); + return '/' + (groupKey === 'localized' && baseDirectory + ? from.to('localizedWithBaseDirectory.' + subKey, baseDirectory, ...args) + : from.to(targetFullKey, ...args)); + }; + // TODO: Is there some nicer way to define these, // may8e without totally re-8inding everything for // each page? @@ -2055,28 +2110,44 @@ async function main() { to }); - const pageFn = () => page({ + const pageInfo = page({ ...bound, language, + + absoluteTo, + relativeTo: to, to, urls, getSizeOfAdditionalFile, }); - const content = writePage.html(pageFn, { + const oEmbedJSON = writePage.oEmbedJSON(pageInfo, { + language, + wikiData, + }); + + const oEmbedJSONHref = (oEmbedJSON && wikiData.wikiInfo.canonicalBase) && ( + wikiData.wikiInfo.canonicalBase + urls.from('shared.root').to('shared.path', paths.pathname + OEMBED_JSON_FILE)); + + const html = writePage.html(pageInfo, { defaultLanguage: finalDefaultLanguage, language, languages, localizedPaths, + oEmbedJSONHref, paths, to, transformMultiline: bound.transformMultiline, wikiData }); - return writePage.write(content, {paths}); + return writePage.write({ + html, + oEmbedJSON, + paths, + }); }), ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => { const title = titleFn({ diff --git a/src/util/html.js b/src/util/html.js index 94756984..a9b4bb9b 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -1,7 +1,22 @@ // Some really simple functions for formatting HTML content. -// Non-comprehensive. ::::P -export const selfClosingTags = ['br', 'img']; +// COMPREHENSIVE! +// https://html.spec.whatwg.org/multipage/syntax.html#void-elements +export const selfClosingTags = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'link', + 'meta', + 'source', + 'track', + 'wbr', +]; // Pass to tag() as an attri8utes key to make tag() return a 8lank string // if the provided content is empty. Useful for when you'll only 8e showing |