« get me outta code hell

social embeds (discord, maybe twitter) - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2022-05-15 23:50:42 -0300
committer(quasar) nebula <qznebula@protonmail.com>2022-05-15 23:50:42 -0300
commitebded2fc40c3ff2faead343b9f68d768e6db2c32 (patch)
tree73aee0ff6437053f11fd63691945b6b0e62d6557
parent44104a8970a10dff2894745f0c904644ca3234e0 (diff)
social embeds (discord, maybe twitter)
Tracks only for now, but more will come Soon(TM)!
-rw-r--r--src/page/track.js38
-rw-r--r--src/strings-default.json8
-rwxr-xr-xsrc/upd8.js89
-rw-r--r--src/util/html.js19
4 files changed, 141 insertions, 13 deletions
diff --git a/src/page/track.js b/src/page/track.js
index 5249ea4..3295d48 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 cc4ec1d..f1a5302 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 12f1af3..b999ef7 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 9475698..a9b4bb9 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