From de2dca0810df435501a257d3cdb31674bcbcfd0d Mon Sep 17 00:00:00 2001 From: Cory Dransfeldt Date: Mon, 16 Sep 2024 07:56:25 -0700 Subject: [PATCH] chore: cleanup --- config/collections/index.js | 12 +- config/filters/feeds.js | 13 ++ config/filters/media.js | 4 +- package-lock.json | 243 +++++++++++++++++++++++++++++- package.json | 13 +- scripts/worker-build.mjs | 34 +++-- src/assets/styles/feed.xsl | 8 +- src/assets/styles/pages/feeds.css | 1 - src/data/albumReleases.js | 19 ++- src/data/concerts.js | 1 + 10 files changed, 311 insertions(+), 37 deletions(-) diff --git a/config/collections/index.js b/config/collections/index.js index 06d673fe..3781a944 100644 --- a/config/collections/index.js +++ b/config/collections/index.js @@ -87,6 +87,8 @@ export const processContent = (collection) => { items.forEach((item) => { let attribution let hashTags = tagsToHashtags(item) ? ' ' + tagsToHashtags(item) : '' + if (item['type'] === 'album-release') hashTags = ' #Music #NewMusic' + if (item['type'] === 'concert') hashTags = ' #Music #Concert' // link attribution if properties exist if (item?.['authors']?.['mastodon']) { @@ -108,6 +110,8 @@ export const processContent = (collection) => { // set url for posts - identified as slugs here if (item?.['slug']) content['url'] = new URL(item['slug'], BASE_URL).toString() + // set unique concert urls + if (item?.['type'] === 'concert') content['url'] = `${item['artistUrl']}?t=${DateTime.fromISO(item['date']).toMillis()}#concerts` if (item?.['description']) { content['description'] = `${item['description'].split(' ').length >= 25 ? item['description'].split(' ').slice(0, 25).join(' ') + '...' : item['description']}` } else if (item?.['notes']) { @@ -135,6 +139,8 @@ export const processContent = (collection) => { addContent(links, '🔗', (item) => item['title'], (item) => item['date']) addContent(books.all.filter((book) => book['status'] === 'finished'), '📖', (item) => `${item['title']}${item['rating'] ? ' (' + item['rating'] + ')' : ''}`, (item) => item['date']) addContent(movies['movies'], '🎥', (item) => `${item['title']}${item['rating'] ? ' (' + item['rating'] + ')' : ''}`, (item) => item['lastWatched']) + addContent(concerts, '🎤', (item) => `${item['artistNameString'] ? item['artistNameString'] : item['artist']['name']} at ${item['venue']['name'].split(',')[0].trim()}`, (item) => item['date']) + addContent([...albumReleases['current']].reverse(), '📆', (item) => `${item['title']} by ${item['artist']['name']}`, (item) => item['release_date']) addSiteMapContent(posts, (item) => item.title, (item) => item.date) addSiteMapContent(pages, (item) => item.title, (item) => item.date) @@ -173,10 +179,10 @@ export const albumReleasesCalendar = (collection) => { start: [date.year, date.month, date.day], startInputType: 'local', startOutputType: 'local', - title: `Release: ${album.artist} - ${album.title}`, - description: `Check out this new album release: ${album.url}`, + title: `Release: ${album['artist']['name']} - ${album['title']}`, + description: `Check out this new album release: ${album['url']}. Read more about ${album['artist']['name']} at https://coryd.dev${album['artist']['url']}`, url: album.url, - uid: `${date.toFormat('yyyyMMdd')}-${album.artist}-${album.title}@coryd.dev`, + uid: `${date.toFormat('yyyyMMdd')}-${album['artist']['name']}-${album.title}@coryd.dev`, timestamp: DateTime.now().toUTC().toFormat("yyyyMMdd'T'HHmmss'Z'") } }).filter(event => event !== null) diff --git a/config/filters/feeds.js b/config/filters/feeds.js index e2b15c66..d6ecb807 100644 --- a/config/filters/feeds.js +++ b/config/filters/feeds.js @@ -3,6 +3,7 @@ import markdownIt from 'markdown-it' import markdownItAnchor from 'markdown-it-anchor' import markdownItFootnote from 'markdown-it-footnote' import sanitizeHtml from 'sanitize-html' +import truncate from 'truncate-html' const BASE_URL = 'https://coryd.dev' @@ -63,6 +64,16 @@ export default { } else if (['book', 'movie'].includes(type)) { processedEntry['excerpt'] = sanitizeHtml(`${md.render(description)}`) } + if (type === 'album-release') { + let sanitizedDescription = sanitizeHtml(`${md.render(description)}`) + let truncatedDescription = truncate(sanitizedDescription, { + length: 500, + reserveLastWord: true, + ellipsis: '...' + }) + if (sanitizedDescription.length > 500) truncatedDescription += ` Read more about ${entry['artist']['name']}` + processedEntry['excerpt'] = truncatedDescription + } if (slug && content) processedEntry['excerpt'] = sanitizeHtml(`${md.render(content)}${feedNote}`, { disallowedTagsMode: 'completelyDiscard' }) @@ -71,6 +82,8 @@ export default { if (rating) processedEntry['rating'] = rating if (tags) processedEntry['tags'] = tags + if (type === 'album-release' && artist) processedEntry['title'] = `${title} by ${artist}` + if (entry) posts.push(processedEntry) }) diff --git a/config/filters/media.js b/config/filters/media.js index 7c603e62..c86981fc 100644 --- a/config/filters/media.js +++ b/config/filters/media.js @@ -28,8 +28,8 @@ export default { break case 'album-release': normalized.title = item['title'] - normalized.alt = `${item['title']} by ${item['artist']}` - normalized.subtext = `${item['artist']} / ${item['date']}` + normalized.alt = `${item['title']} by ${item['artist']['name']}` + normalized.subtext = `${item['artist']['name']} / ${item['date']}` break case 'movie': normalized.title = item['title'] diff --git a/package-lock.json b/package-lock.json index 85755699..571c727c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coryd.dev", - "version": "24.11.0", + "version": "24.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coryd.dev", - "version": "24.11.0", + "version": "24.13.0", "license": "MIT", "dependencies": { "@cdransf/api-text": "^1.5.0", @@ -23,6 +23,7 @@ "autoprefixer": "^10.4.20", "cssnano": "^7.0.6", "dotenv-flow": "^4.1.0", + "fast-xml-parser": "^4.5.0", "html-entities": "^2.5.2", "html-minifier-terser": "^7.2.0", "ics": "^3.7.6", @@ -38,7 +39,8 @@ "rimraf": "^6.0.1", "sanitize-html": "^2.13.0", "slugify": "^1.6.6", - "terser": "^5.32.0" + "terser": "^5.32.0", + "truncate-html": "^1.1.2" } }, "node_modules/@11ty/dependency-tree": { @@ -1093,6 +1095,158 @@ "dev": true, "license": "MIT" }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1939,6 +2093,29 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3152,6 +3329,49 @@ "dev": true, "license": "MIT" }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4631,6 +4851,13 @@ "node": ">=0.10.0" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true, + "license": "MIT" + }, "node_modules/stylehacks": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.4.tgz", @@ -4774,6 +5001,16 @@ "dev": true, "license": "MIT" }, + "node_modules/truncate-html": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/truncate-html/-/truncate-html-1.1.2.tgz", + "integrity": "sha512-BiLzO594/Quf0wu3jHnVxHA4X5tl4Gunhqe2mlGTa5ElwHJGw7M/N5JdBvU8OPtR+MaEIvmyUdNxnoEi3YI5Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.12" + } + }, "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", diff --git a/package.json b/package.json index fd11a0d6..98e71cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coryd.dev", - "version": "24.11.0", + "version": "24.13.0", "description": "The source for my personal site. Built using 11ty (and other tools).", "type": "module", "scripts": { @@ -10,11 +10,8 @@ "update:deps": "npm upgrade && ncu", "debug": "DEBUG=Eleventy* npx @11ty/eleventy --serve", "clean": "rimraf _site", - "publish:analytics": "node scripts/worker-build.mjs analytics && wrangler deploy --env production --config workers/analytics/wrangler.toml", - "publish:contact": "node scripts/worker-build.mjs contact && wrangler deploy --env production --config workers/contact/wrangler.toml", - "publish:playing": "node scripts/worker-build.mjs playing && wrangler deploy --env production --config workers/playing/wrangler.toml", - "publish:rebuild": "node scripts/worker-build.mjs rebuild && wrangler deploy --env production --config workers/rebuild/wrangler.toml", - "publish:scrobble": "node scripts/worker-build.mjs scrobble && wrangler deploy --env production --config workers/scrobble/wrangler.toml" + "build:worker": "node scripts/worker-build.mjs $WORKER_NAME", + "deploy:worker": "wrangler deploy --env production --config workers/$npm_config_worker/wrangler.toml" }, "keywords": [ "11ty", @@ -39,6 +36,7 @@ "autoprefixer": "^10.4.20", "cssnano": "^7.0.6", "dotenv-flow": "^4.1.0", + "fast-xml-parser": "^4.5.0", "html-entities": "^2.5.2", "html-minifier-terser": "^7.2.0", "ics": "^3.7.6", @@ -54,6 +52,7 @@ "rimraf": "^6.0.1", "sanitize-html": "^2.13.0", "slugify": "^1.6.6", - "terser": "^5.32.0" + "terser": "^5.32.0", + "truncate-html": "^1.1.2" } } diff --git a/scripts/worker-build.mjs b/scripts/worker-build.mjs index 092b58fe..bc21a566 100644 --- a/scripts/worker-build.mjs +++ b/scripts/worker-build.mjs @@ -1,22 +1,28 @@ -import fs from 'fs/promises'; -import dotenv from 'dotenv-flow'; +import fs from 'fs/promises' +import dotenv from 'dotenv-flow' -dotenv.config(); +dotenv.config() -const workerName = process.argv[2]; +const workerName = process.argv[2] if (!workerName) { - console.error('Please specify a worker name.'); - process.exit(1); + console.error('Please specify a worker name.') + process.exit(1) } -const templatePath = `workers/${workerName}/wrangler.template.toml`; -const outputPath = `workers/${workerName}/wrangler.toml`; -const template = await fs.readFile(templatePath, 'utf8'); -const output = template - .replace(/\${CF_ACCOUNT_ID}/g, process.env.CF_ACCOUNT_ID) - .replace(/\${CF_ZONE_ID}/g, process.env.CF_ZONE_ID); +const templatePath = `workers/${workerName}/wrangler.template.toml` +const outputPath = `workers/${workerName}/wrangler.toml` -await fs.writeFile(outputPath, output); +try { + const template = await fs.readFile(templatePath, 'utf8') + const output = template + .replace(/\${CF_ACCOUNT_ID}/g, process.env.CF_ACCOUNT_ID) + .replace(/\${CF_ZONE_ID}/g, process.env.CF_ZONE_ID) -console.log(`Generated wrangler.toml for ${workerName}`); \ No newline at end of file + await fs.writeFile(outputPath, output) + + console.log(`Generated wrangler.toml for ${workerName}`) +} catch (error) { + console.error('Error generating wrangler.toml:', error) + process.exit(1) +} \ No newline at end of file diff --git a/src/assets/styles/feed.xsl b/src/assets/styles/feed.xsl index 4899495c..52e33a05 100644 --- a/src/assets/styles/feed.xsl +++ b/src/assets/styles/feed.xsl @@ -45,11 +45,11 @@ - + diff --git a/src/assets/styles/pages/feeds.css b/src/assets/styles/pages/feeds.css index 24d99ae9..b31cc7b1 100644 --- a/src/assets/styles/pages/feeds.css +++ b/src/assets/styles/pages/feeds.css @@ -43,6 +43,5 @@ & footer { padding-bottom: var(--spacing-3xl); - width: unset; } } \ No newline at end of file diff --git a/src/data/albumReleases.js b/src/data/albumReleases.js index 1de7c8a5..7ad04c10 100644 --- a/src/data/albumReleases.js +++ b/src/data/albumReleases.js @@ -1,4 +1,5 @@ import { createClient } from '@supabase/supabase-js' +import { sanitizeMediaString, parseCountryField } from '../../config/utilities/index.js' import { DateTime } from 'luxon' const SUPABASE_URL = process.env.SUPABASE_URL @@ -15,7 +16,11 @@ const fetchAlbumReleases = async () => { release_link, total_plays, art, - artist_name + artist_name, + artist_description, + artist_total_plays, + artist_country, + artist_favorite `) if (error) { @@ -27,10 +32,17 @@ const fetchAlbumReleases = async () => { const releaseDate = DateTime.fromISO(album['release_date']).toUTC().startOf('day') return { - artist: album['artist_name'], + artist: { + name: album['artist_name'], + description: album['artist_description'], + total_plays: album['artist_total_plays'], + country: album['artist_country'], + favorite: album['artist_favorite'], + url: `/music/artists/${sanitizeMediaString(album['artist_name'])}-${sanitizeMediaString(parseCountryField(album['artist_country']))}`, + }, title: album['name'], date: releaseDate.toLocaleString(DateTime.DATE_FULL), - description: 'Check out the new release!', + description: album['artist_description'], url: album['release_link'], image: album['art'] ? `/${album['art']}` : '', total_plays: album['total_plays'], @@ -39,6 +51,7 @@ const fetchAlbumReleases = async () => { timestamp: releaseDate.toSeconds(), } }).sort((a, b) => a['timestamp'] - b['timestamp']) + const upcoming = all.filter(album => (!album['total_plays'] || album['total_plays'] <= 0) && album['release_date'] > today) const current = all.filter(album => album['release_date'] <= today) diff --git a/src/data/concerts.js b/src/data/concerts.js index 533af1a1..f9d58dcb 100644 --- a/src/data/concerts.js +++ b/src/data/concerts.js @@ -57,6 +57,7 @@ const processConcerts = (concerts) => { boundingBox: concert['bounding_box'], notes: concert['venue_notes'] }, + description: 'I went to (yet another) concert!', notes: concert['concert_notes'], artist: concert['artist'] ? { name: concert['artist_name'],