diff --git a/.eleventy.js b/.eleventy.js index 584bef66..a56b5484 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -89,7 +89,6 @@ export default async function (eleventyConfig) { sortAttributes: true, sortClassName: true, useShortDoctype: true, - processScripts: ['application/ld+json'], }) } return content diff --git a/package-lock.json b/package-lock.json index 368cac8c..6598367a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "html-minifier-terser": "^7.2.0", "html-to-text": "^9.0.5", "ics": "^3.8.1", + "linkedom": "0.18.5", "luxon": "^3.5.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.2.0", @@ -1418,6 +1419,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -2198,6 +2206,13 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, "node_modules/html-minifier-terser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", @@ -2554,6 +2569,53 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linkedom": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.5.tgz", + "integrity": "sha512-JGLaGGtqtu+eOhYrC1wkWYTBcpVWL4AsnwAtMtgO1Q0gI0PuPJKI0zBBE+a/1BrhOE3Uw8JI/ycByAv5cLrAuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^9.1.0", + "uhyphen": "^0.2.0" + } + }, + "node_modules/linkedom/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/linkedom/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "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.1.0", + "entities": "^4.5.0" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -4570,6 +4632,13 @@ "dev": true, "license": "MIT" }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "dev": true, + "license": "ISC" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", diff --git a/package.json b/package.json index 580d0e64..f3580513 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coryd.dev", - "version": "1.1.1", + "version": "1.2.0", "description": "The source for my personal site. Built using 11ty (and other tools).", "type": "module", "engines": { @@ -44,6 +44,7 @@ "html-minifier-terser": "^7.2.0", "html-to-text": "^9.0.5", "ics": "^3.8.1", + "linkedom": "0.18.5", "luxon": "^3.5.0", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.2.0", diff --git a/src/assets/styles/pages/articles.css b/src/assets/styles/pages/articles.css index e3f84035..5ef28ff9 100644 --- a/src/assets/styles/pages/articles.css +++ b/src/assets/styles/pages/articles.css @@ -11,7 +11,7 @@ article { } & h3, - &:not(:has(h3)) p { + &:not(:has(h3)) p:first-of-type { margin-top: 0; } diff --git a/src/data/movies.js b/src/data/movies.js index 343a1238..a83e332e 100644 --- a/src/data/movies.js +++ b/src/data/movies.js @@ -36,7 +36,11 @@ export default async function () { try { const movies = await fetchAllMovies() const favoriteMovies = movies.filter(movie => movie['favorite']) - const recentlyWatchedMovies = movies.filter(movie => movie['last_watched'] && year - DateTime.fromISO(movie['last_watched']).year <= 3) + const now = DateTime.now(); + const recentlyWatchedMovies = movies.filter(movie => { + const lastWatched = movie['last_watched'] + return (lastWatched && now.diff(DateTime.fromISO(lastWatched), 'months').months <= 6) + }) return { movies, diff --git a/src/data/tv.js b/src/data/tv.js index 06a9e4e1..ccc1f48a 100644 --- a/src/data/tv.js +++ b/src/data/tv.js @@ -46,7 +46,7 @@ export default async function () { return { shows, - recentlyWatched: episodes.slice(0, 225), + recentlyWatched: episodes.slice(0, 125), favorites: shows.filter(show => show.favorite).sort((a, b) => a.title.localeCompare(b.title)), } } catch (error) { diff --git a/src/includes/base.liquid b/src/includes/base.liquid index 71557765..0ad225be 100644 --- a/src/includes/base.liquid +++ b/src/includes/base.liquid @@ -4,26 +4,12 @@ {%- assign pageTitle = post.title -%} {%- elsif title -%} {%- assign pageTitle = title | append: ' / ' | append: globals.site_name -%} -{%- elsif artist.name -%} - {%- assign pageTitle = 'Artists / ' | append: artist.name | append: ' / ' | append: globals.site_name -%} {%- elsif schema == 'music-index' -%} {%- assign pageTitle = 'Music / ' | append: globals.site_name -%} {%- elsif schema == 'music-period' -%} {%- assign pageTitle = 'Music / ' | append: page.title | append: globals.site_name -%} -{%- elsif genre.name -%} - {%- assign pageTitle = 'Music / ' | append: genre.name | append: ' / ' | append: globals.site_name -%} -{%- elsif book.title -%} - {%- assign pageTitle = 'Books / ' | append: book.title | append: ' by ' | append: book.author | append: ' / ' | append: globals.site_name -%} {%- elsif year.value -%} {%- assign pageTitle = ' / Books ' | prepend: year.value | append: ' / ' | append: globals.site_name -%} -{%- elsif movie.title -%} - {%- assign pageTitle = 'Movies / ' | append: movie.title -%} - {%- if movie.rating -%} - {%- assign pageTitle = pageTitle | append: ' (' | append: movie.rating | append: ')' -%} - {%- endif -%} - {%- assign pageTitle = pageTitle | append: ' / ' | append: globals.site_name -%} -{%- elsif show.title -%} - {%- assign pageTitle = 'Shows / ' | append: show.title | append: ' / ' | append: globals.site_name -%} {%- elsif page.description -%} {%- assign pageTitle = page.title | append: ' / ' | append: globals.site_name -%} {%- endif -%} @@ -31,16 +17,6 @@ {%- assign pageDescription = globals.site_description -%} {%- if schema == 'blog' -%} {%- assign pageDescription = post.description | markdown | strip_html -%} -{%- elsif artist.description -%} - {%- assign pageDescription = artist.description | markdown | strip_html | htmlTruncate -%} -{%- elsif book.description -%} - {%- assign pageDescription = book.review | markdown | strip_html | default: book.description | htmlTruncate -%} -{%- elsif movie.description -%} - {%- assign pageDescription = movie.review | markdown | strip_html | default: movie.description | htmlTruncate -%} -{%- elsif show.description -%} - {%- assign pageDescription = show.review | markdown | strip_html | default: show.description | htmlTruncate -%} -{%- elsif genre.description -%} - {%- assign pageDescription = genre.description | markdown | strip_html | htmlTruncate -%} {%- elsif page.description -%} {%- assign pageDescription = page.description -%} {%- elsif description -%} @@ -53,10 +29,6 @@ {%- assign ogImage = globals.cdn_url | append: page.image -%} {%- when 'music-index' -%} {%- assign ogImage = globals.cdn_url | append: music.week.artists[0].grid.image -%} - {%- when 'artist' -%} - {%- assign ogImage = globals.cdn_url | append: artist.grid.image -%} - {%- when 'genre' -%} - {%- assign ogImage = globals.cdn_url | append: genre.artists[0].grid.image -%} {%- when 'watching' -%} {%- assign featuredMovie = movies.recentlyWatched | first -%} {%- assign ogImage = globals.cdn_url | append: featuredMovie.grid.backdrop -%} @@ -75,15 +47,6 @@ {%- when 'books-year' -%} {%- assign featuredBook = books.currentYear | first -%} {%- assign ogImage = globals.cdn_url | append: featuredBook.grid.image -%} - {%- when 'book' -%} - {%- assign ogImage = globals.cdn_url | append: book.grid.image -%} - {%- when 'movie' -%} - {%- assign ogImage = globals.cdn_url | append: movie.grid.backdrop -%} - {%- when 'show' -%} - {%- assign ogImage = globals.cdn_url | append: show.grid.backdrop -%} - {%- when 'genre' -%} - {%- assign genreArtist = genre.artists | shuffleArray | first -%} - {%- assign ogImage = globals.cdn_url | append: genreArtist.grid.image -%} {%- endcase -%} {%- assign escapedPageDescription = pageDescription | escape -%} diff --git a/src/pages/dynamic/books/book.html b/src/pages/dynamic/books/book.html deleted file mode 100644 index 7d67eb8f..00000000 --- a/src/pages/dynamic/books/book.html +++ /dev/null @@ -1,70 +0,0 @@ ---- -layout: default -pagination: - data: books.all - size: 1 - alias: book -permalink: "{{ book.url }}/index.html" -schema: book ---- -{%- capture alt -%} - {{ book.title }}{% if book.author %} by {{ book.author }}{% endif %} -{%- endcapture -%} -{% tablericon "arrow-left" %} Back to books -
-
- {{ alt | replaceQuotes }} -
-

{{ book.title }}

- {% if book.rating %}

{{ book.rating }}

{% endif %} - {% if book.author %} -

By {{ book.author }}

- {% endif %} - {%- if book.favorite -%} -

{% tablericon "heart" %} This is one of my favorite books!

- {%- endif -%} - {%- if book.tattoo -%} -

{% tablericon "needle" %} I have a tattoo inspired by this book!

- {%- endif -%} - {% if book.status == 'finished' %} -

Finished on: {{ book.date_finished | date: "%B %e, %Y" }}

- {% endif %} - {% if book.status == 'started' %} - {%- assign percentage = book.progress | append: '%' -%} - {% render "partials/media/progress-bar.liquid", percentage:percentage %} - {% endif %} -
-
- {% if book.review %} - {% render "partials/blocks/banners/warning.liquid", text: "There are probably spoilers after this banner — this is a warning about them." %} -

My thoughts

- {{ book.review | markdown }} -
- {% endif %} - {% render "partials/blocks/associated-media.liquid", posts:book.posts %} - {% render "partials/blocks/associated-media.liquid", books:book.related_books %} - {% render "partials/blocks/associated-media.liquid", artists:book.artists %} - {% render "partials/blocks/associated-media.liquid", movies:book.movies %} - {% render "partials/blocks/associated-media.liquid", shows:book.shows %} - {% render "partials/blocks/associated-media.liquid", genres:book.genres %} - {% if book.description %} -

Overview

- {{ book.description | markdown }} - {% endif %} -
\ No newline at end of file diff --git a/src/pages/dynamic/music/artists/artist.html b/src/pages/dynamic/music/artists/artist.html deleted file mode 100644 index 450e7f6f..00000000 --- a/src/pages/dynamic/music/artists/artist.html +++ /dev/null @@ -1,114 +0,0 @@ ---- -layout: default -pagination: - data: artists - size: 1 - alias: artist -permalink: "{{ artist.url }}/index.html" -updated: "now" -schema: artist ---- -{%- capture alt -%} - {{ artist.name }} / {{ artist.country }} -{%- endcapture -%} -{%- capture playLabel -%} - {%- if artist.total_plays == 1 -%} - play - {%- else -%} - plays - {%- endif -%} -{%- endcapture -%} - -{% tablericon "arrow-left" %} Back to music -
-
- {{ alt | replaceQuotes }} -
-

{{ artist.name }}

-

{% tablericon "map-pin" %} {{ artist.country }}

- {%- if artist.favorite -%} -

{% tablericon "heart" %} This is one of my favorite artists!

- {%- endif -%} - {%- if artist.tattoo -%} -

{% tablericon "needle" %} I have a tattoo inspired by this artist!

- {%- endif -%} - {%- if artist.total_plays > 0 -%} -

{{ artist.total_plays | formatNumber }} {{ playLabel }}

- {%- endif -%} -

- - {{ artist.genre.name }} - -

-
-
- {% render "partials/blocks/associated-media.liquid", posts:artist.posts %} - {% render "partials/blocks/associated-media.liquid", artists:artist.related_artists %} - {% render "partials/blocks/associated-media.liquid", books:artist.books %} - {% render "partials/blocks/associated-media.liquid", movies:artist.movies %} - {% render "partials/blocks/associated-media.liquid", shows:artist.shows %} - {%- if artist.description -%} -

Overview

-
{{ artist.description | markdown }}
- - {%- endif -%} - {%- if artist.concerts -%} -
-

- {% tablericon "device-speaker" %} - I've seen this artist live! -

- - {%- endif -%} - {%- if artist.books or artist.concerts or artist.movies -%}
{%- endif -%} - - - - - - - {% for album in artist.albums %} - - - - - - {% endfor %} -
AlbumPlaysYear
{{ album.name }}{{ album.total_plays }}{{ album.release_year }}
-

These are the albums by this artist that are in my collection, not necessarily a comprehensive discography.

-
\ No newline at end of file diff --git a/src/pages/dynamic/watching/movie.html b/src/pages/dynamic/watching/movie.html deleted file mode 100644 index 9e3573de..00000000 --- a/src/pages/dynamic/watching/movie.html +++ /dev/null @@ -1,69 +0,0 @@ ---- -layout: default -pagination: - data: movies.movies - size: 1 - alias: movie -permalink: "{{ movie.url }}/index.html" -schema: movie ---- -{%- capture alt -%} - {{ movie.title }} / {{ movie.year }}{% if move.rating %} ({{ movie.rating }}){% endif %} -{%- endcapture -%} -{% tablericon "arrow-left" %} Back to watching -
- {{ alt | replaceQuotes }} -
-

{{ movie.title }}{%- if movie.year and not movie.rating %} ({{ movie.year }}){%- endif -%}

- {%- if movie.rating -%} -

- {{ movie.rating }} - {%- if movie.year %} - ({{ movie.year }}) - {%- endif -%} -

- {% endif -%} - {%- if movie.favorite -%} -

{% tablericon "heart" %} This is one of my favorite movies!

- {%- endif -%} - {%- if movie.tattoo -%} -

{% tablericon "needle" %} I have a tattoo inspired by this movie!

- {%- endif -%} - {%- if movie.collected -%} -

{% tablericon "circle-check" %} This movie is in my collection!

- {%- endif -%} - {%- if movie.lastWatched -%}

Last watched on {{ movie.lastWatched | date: "%B %e, %Y" }}.

{%- endif -%} -
- {% if movie.review %} - {% render "partials/blocks/banners/warning.liquid", text: "There are probably spoilers after this banner — this is a warning about them." %} -

My thoughts

- {{ movie.review | markdown }} -
- {% endif %} - {% render "partials/blocks/associated-media.liquid", posts:movie.posts %} - {% render "partials/blocks/associated-media.liquid", movies:movie.related_movies %} - {% render "partials/blocks/associated-media.liquid", shows:movie.shows %} - {% render "partials/blocks/associated-media.liquid", artists:movie.artists %} - {% render "partials/blocks/associated-media.liquid", books:movie.books %} - {% render "partials/blocks/associated-media.liquid", genres:movie.genres %} - {% if movie.description %} -

Overview

- {{ movie.description | markdown }} - {% endif %} -
\ No newline at end of file diff --git a/src/pages/dynamic/watching/show.html b/src/pages/dynamic/watching/show.html deleted file mode 100644 index 06a3e751..00000000 --- a/src/pages/dynamic/watching/show.html +++ /dev/null @@ -1,65 +0,0 @@ ---- -layout: default -pagination: - data: tv.shows - size: 1 - alias: show -permalink: "{{ show.url }}/index.html" -schema: show ---- -{%- capture alt -%} - {{ show.title }} / {{ show.year }} -{%- endcapture -%} -{% tablericon "arrow-left" %} Back to watching -
- {{ alt | replaceQuotes }} -
-

{{ show.title }}{%- if show.year %} ({{ show.year }}){%- endif -%}

- {%- if show.favorite -%} -

{% tablericon "heart" %} This is one of my favorite shows!

- {%- endif -%} - {%- if show.tattoo -%} -

{% tablericon "needle" %} I have a tattoo inspired by this show!

- {%- endif -%} - {%- if show.collected -%} -

{% tablericon "circle-check" %} This show is in my collection!

- {%- endif -%} - {%- if show.episode.formatted_episode -%} - {%- capture lastWatchedText -%} - I last watched {{ show.episode.formatted_episode }} on {{ show.episode.last_watched_at | date: "%B %e, %Y" }}. - {%- endcapture -%} -

{{ lastWatchedText }}

- {%- endif -%} -
- {% if show.review %} - {% render "partials/blocks/banners/warning.liquid", text: "There are probably spoilers after this banner — this is a warning about them." %} -

My thoughts

- {{ show.review | markdown }} -
- {% endif %} - {% render "partials/blocks/associated-media.liquid", posts:show.posts %} - {% render "partials/blocks/associated-media.liquid", artists:show.artists %} - {% render "partials/blocks/associated-media.liquid", shows:show.related_shows %} - {% render "partials/blocks/associated-media.liquid", movies:show.movies %} - {% render "partials/blocks/associated-media.liquid", books:show.books %} - {% if show.description %} -

Overview

- {{ show.description | markdown }} - {% endif %} -
\ No newline at end of file diff --git a/workers/dynamic-pages/index.js b/workers/dynamic-pages/index.js new file mode 100644 index 00000000..4f6250ef --- /dev/null +++ b/workers/dynamic-pages/index.js @@ -0,0 +1,457 @@ +import { createClient } from '@supabase/supabase-js' +import { parseHTML } from 'linkedom' +import markdownIt from 'markdown-it' +import truncateHtml from 'truncate-html' +import { convert } from 'html-to-text' + +const md = markdownIt({ html: true, linkify: true }) +const ICON_MAP = { + alertTriangle: ``, + arrowLeft: ``, + article: ``, + books: ``, + circleCheck: ``, + circleX: ``, + deviceSpeaker: ``, + deviceTvOld: ``, + film: ``, + headphones: ``, + heart: ``, + infoCircle: ``, + link: ``, + mapPin: ``, + needle: ``, + movie: ``, +} + +const regionNames = new Intl.DisplayNames(['en'], { type: 'region' }) +const getCountryName = (countryCode) => regionNames.of(countryCode.trim()) || countryCode.trim() +const parseCountryField = (countryField) => { + if (!countryField) return null + const delimiters = [',', '/', '&', 'and'] + let countries = [countryField] + delimiters.forEach(delimiter => countries = countries.flatMap(country => country.split(delimiter))) + return countries.map(getCountryName).join(', ') +} + + +const generateMediaLinks = (data, type, count = 10) => { + if (!data || !type) return '' + + const dataSlice = data.slice(0, count) + if (dataSlice.length === 0) return null + + const buildLink = (item) => { + switch (type) { + case 'genre': + return `${item['genre_name']}` + case 'artist': + return `${item['name']}` + case 'book': + return `${item['title']}` + default: + return '' + } + } + + if (dataSlice.length === 1) return buildLink(dataSlice[0]) + + const links = dataSlice.map(buildLink) + const allButLast = links.slice(0, -1).join(', ') + const last = links[links.length - 1] + + return `${allButLast} and ${last}` +} + +async function fetchDataByUrl(supabase, table, url) { + const { data, error } = await supabase.from(table).select('*').eq('url', url).single() + + if (error) { + console.error(`Error fetching from ${table}:`, error) + return null + } + + return data +} + +async function fetchGlobals(supabase) { + const { data, error } = await supabase.from('optimized_globals').select('*').single() + if (error) { + console.error('Error fetching globals:', error) + return {} + } + return data +} + +function generateMetadata(data, type, globals) { + let title = globals['site_name'] + let description = data.description || globals.site_description + const canonicalUrl = data.url ? `${globals.url}${data.url}` : globals.url + const ogImage = `${globals.cdn_url}${data.image || globals.avatar}?class=w800` + + description = convert(truncateHtml(md.render(description), 100, { + byWords: true, + ellipsis: '...' + }), + { + wordwrap: false, + selectors: [ + { selector: 'a', options: { ignoreHref: true } }, + { selector: 'h1', options: { uppercase: false } }, + { selector: 'h2', options: { uppercase: false } }, + { selector: 'h3', options: { uppercase: false } }, + { selector: '*', format: 'block' } + ] + }).replace(/\s+/g, ' ').trim() + + switch (type) { + case 'artist': + title = `Artists / ${data['name']} / ${globals['site_name']}` + break + case 'genre': + title = `Genre / ${data['name']} / ${globals['site_name']}` + break + case 'book': + title = `Books / ${data['title']} by ${data.author} / ${globals['site_name']}` + break + case 'movie': + title = `Movies / ${data['title']} (${data.year}) / ${globals['site_name']}` + break + case 'show': + title = `Shows / ${data['title']} / ${globals['site_name']}` + break + default: + title = `${data['title'] || globals['site_name']}` + } + + return { + title, + description, + 'og:title': title, + 'og:description': description, + 'og:image': ogImage, + 'og:url': canonicalUrl, + 'canonical': canonicalUrl + } +} + +function updateDynamicContent(html, metadata, mediaHtml) { + const { document } = parseHTML(html) + + const titleTag = document.querySelector('title[data-dynamic="title"]') + if (titleTag) titleTag.textContent = metadata['title'] + + const dynamicMetaSelectors = [ + { selector: 'meta[data-dynamic="description"]', attribute: 'content', value: metadata.description }, + { selector: 'meta[data-dynamic="og:title"]', attribute: 'content', value: metadata['og:title'] }, + { selector: 'meta[data-dynamic="og:description"]', attribute: 'content', value: metadata['og:description'] }, + { selector: 'meta[data-dynamic="og:image"]', attribute: 'content', value: metadata['og:image'] }, + { selector: 'meta[data-dynamic="og:url"]', attribute: 'content', value: metadata.canonical }, + ] + + dynamicMetaSelectors.forEach(({ selector, attribute, value }) => { + const element = document.querySelector(selector) + if (element) element.setAttribute(attribute, value) + }) + + const canonicalLink = document.querySelector('link[rel="canonical"]') + if (canonicalLink) canonicalLink.setAttribute('href', metadata.canonical) + + const pageElement = document.querySelector('[data-dynamic="page"]') + if (pageElement) pageElement.innerHTML = mediaHtml + + return document.toString() +} + +const warningBanner = `` + +function generateAssociatedMediaHTML(data, isGenre = false) { + const sections = [ + { key: 'artists', icon: 'headphones', category: 'music', title: 'Related Artist(s)' }, + { key: 'books', icon: 'books', category: 'books', title: 'Related Book(s)' }, + { key: 'genres', icon: 'headphones', category: 'music', title: 'Related Genre(s)' }, + { key: 'related_movies', icon: 'film', category: 'movies', title: 'Related Movie(s)' }, + { key: 'posts', icon: 'article', category: 'article', title: 'Related Post(s)' }, + { key: 'shows', icon: 'deviceTvOld', category: 'tv', title: 'Related Show(s)' } + ] + + return sections + .filter(({ key }) => !(isGenre && key === 'artists')) + .map(({ key, category, icon, title }) => + data[key] && data[key].length + ? `
+

${ICON_MAP[icon]} ${title}

+ +
` + : '' + ) + .join('') +} + +function generateWatchingHTML(media, globals, type) { + const isShow = type === 'show' + const label = isShow ? 'show' : 'movie' + const lastWatched = media.lastWatched || (isShow && media.episode?.last_watched_at) + + return ` + ${ICON_MAP.arrowLeft} Back to watching +
+ ${media.title} / ${media.year} +
+

${media.title} (${media.year})

+ ${media.favorite ? `

${ICON_MAP.heart} This is one of my favorite ${label}s!

` : ''} + ${media.tattoo ? `

${ICON_MAP.needle} I have a tattoo inspired by this ${label}!

` : ''} + ${media.collected ? `

${ICON_MAP.circleCheck} This ${label} is in my collection!

` : ''} + ${lastWatched ? `

Last watched on ${new Date(lastWatched).toLocaleDateString()}

` : ''} +
+ ${media.review ? `${warningBanner}

My thoughts

${md.render(media.review)}

` : ''} + ${generateAssociatedMediaHTML(media)} + ${media.description ? `

Overview

${md.render(media.description)}

` : ''} +
+ ` +} + +function generateConcertModal(concert) { + const venue = concert.venue_name + ? concert.venue_latitude && concert.venue_longitude + ? `${concert.venue_name_short}` + : concert.venue_name_short + : '' + + const notesModal = concert.notes + ? ` + + ` + : '' + + return ` +
  • + ${new Date(concert.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })} at ${venue} + ${notesModal} +
  • + ` +} + +function generateArtistHTML(artist, globals) { + const playLabel = artist?.total_plays === 1 ? 'play' : 'plays' + const concertsList = artist.concerts?.length + ? `
    +

    + ${ICON_MAP['deviceSpeaker']} + I've seen this artist live! +

    + ` + : '' + const albumsTable = artist.albums?.length + ? ` + + ${artist.albums.map(album => ` + + + + + `).join('')} +
    AlbumPlaysYear
    ${album.name}${album.total_plays || 0}${album.release_year}
    +

    These are the albums by this artist that are in my collection, not necessarily a comprehensive discography.

    + ` + : '' + + return ` + ${ICON_MAP.arrowLeft} Back to music +
    +
    + ${artist.name} / ${artist.country} +
    +

    ${artist.name}

    +

    ${ICON_MAP['mapPin']} ${parseCountryField(artist.country)}

    + ${artist.favorite ? `

    ${ICON_MAP['heart']} This is one of my favorite artists!

    ` : ''} + ${artist.tattoo ? `

    ${ICON_MAP['needle']} I have a tattoo inspired by this artist!

    ` : ''} + ${artist.total_plays ? `

    ${artist.total_plays} ${playLabel}

    ` : ''} +

    ${artist.genre ? `${artist.genre.name}` : ''}

    +
    +
    + ${artist.description ? ` +

    Overview

    +
    ${md.render(artist.description)}
    + ` : '' + } + ${concertsList} + ${albumsTable} +
    + ` +} + +function generateBookHTML(book, globals) { + const alt = `${book.title}${book.author ? ` by ${book.author}` : ''}` + const percentage = book.progress ? `${book.progress}%` : '' + const status = book.status === 'finished' + ? `Finished on ${new Date(book.date_finished).toLocaleDateString()}` + : percentage + ? `
    +
    +
    ` + : '' + + return ` + ${ICON_MAP.arrowLeft} Back to books +
    +
    + ${alt} +
    +

    ${book.title}

    + ${book.rating ? `

    ${book.rating}

    ` : ''} + ${book.author ? `

    By ${book.author}

    ` : ''} + ${book.favorite ? `

    ${ICON_MAP.heart} This is one of my favorite books!

    ` : ''} + ${book.tattoo ? `

    ${ICON_MAP.needle} I have a tattoo inspired by this book!

    ` : ''} + ${status ? `

    ${status}

    ` : ''} +
    +
    + ${book.review ? `${warningBanner}

    My thoughts

    ${book.review}

    ` : ''} + ${generateAssociatedMediaHTML(book)} +

    Overview

    +

    ${md.render(book.description)}

    +
    + ` +} + +function generateGenreHTML(genre) { + const artistCount = genre.artists?.length || 0 + const connectingWords = artistCount > 1 ? 'artists are' : 'artist is' + const mediaLinks = generateMediaLinks(genre.artists, 'artist', 5) + + return ` + ${ICON_MAP.arrowLeft} Back to music +

    ${genre.name}

    +
    + ${mediaLinks ? ` +

    My top ${genre.name} ${connectingWords} ${mediaLinks}. I've listened to ${genre.total_plays} tracks from this genre.

    +
    ` : ''} + ${generateAssociatedMediaHTML(genre, true)} + ${genre.description ? ` +

    Overview

    +
    + ${md.render(genre.description)} +

    Continue reading at Wikipedia.

    +

    Wikipedia content provided under the terms of the Creative Commons BY-SA license.

    +
    + ` : ''} +
    + ` +} + +export default { + async fetch(request, env) { + const url = new URL(request.url) + const path = url.pathname.replace(/\/$/, '') + const supabaseUrl = env.SUPABASE_URL + const supabaseKey = env.SUPABASE_KEY + const supabase = createClient(supabaseUrl, supabaseKey) + let data, type + + if (path === '/books' || path === '/books/') return fetch('https://coryd.dev/books/') + if (path.startsWith('/books/years/')) return fetch(`https://coryd.dev${path}`) + + if (path.startsWith('/watching/movies/')) { + data = await fetchDataByUrl(supabase, 'optimized_movies', path) + type = 'movie' + } else if (path.startsWith('/watching/shows/')) { + data = await fetchDataByUrl(supabase, 'optimized_shows', path) + type = 'show' + } else if (path.startsWith('/music/artists/')) { + data = await fetchDataByUrl(supabase, 'optimized_artists', path) + type = 'artist' + } else if (path.startsWith('/music/genres/')) { + data = await fetchDataByUrl(supabase, 'optimized_genres', path) + type = 'genre' + } else if (path.startsWith('/books/')) { + data = await fetchDataByUrl(supabase, 'optimized_books', path) + type = 'book' + } else { + return Response.redirect('https://coryd.dev/404', 302) + } + + if (!data) return Response.redirect('https://coryd.dev/404', 302) + + const globals = await fetchGlobals(supabase) + let mediaHtml + + switch (type) { + case 'artist': + mediaHtml = generateArtistHTML(data, globals) + break + case 'genre': + mediaHtml = generateGenreHTML(data, globals) + break + case 'book': + mediaHtml = generateBookHTML(data, globals) + break + default: + mediaHtml = generateWatchingHTML(data, globals, type) + break + } + + const templateResponse = await fetch('https://coryd.dev/dynamic.html') + const template = await templateResponse.text() + + const metadata = generateMetadata(data, type, globals) + const html = updateDynamicContent(template, metadata, mediaHtml) + const headers = new Headers({ + 'Content-Type': 'text/html', + 'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400', + }) + + return new Response(html, { headers }) + } +} \ No newline at end of file diff --git a/workers/dynamic-pages/wrangler.template.toml b/workers/dynamic-pages/wrangler.template.toml new file mode 100644 index 00000000..5eb10d01 --- /dev/null +++ b/workers/dynamic-pages/wrangler.template.toml @@ -0,0 +1,25 @@ +name = "dynamic-media-worker" +main = "./index.js" +compatibility_date = "2023-01-01" + +account_id = "${CF_ACCOUNT_ID}" +workers_dev = true + +[observability] +enabled = true + +[env.production] +name = "dynamic-media-worker-production" +routes = [ + { pattern = "https://coryd.dev/watching/movies/*", zone_id = "${CF_ZONE_ID}" }, + { pattern = "https://coryd.dev/watching/shows/*", zone_id = "${CF_ZONE_ID}" }, + { pattern = "https://coryd.dev/music/artists/*", zone_id = "${CF_ZONE_ID}" }, + { pattern = "https://coryd.dev/music/genres/*", zone_id = "${CF_ZONE_ID}" }, + { pattern = "https://coryd.dev/books/*", zone_id = "${CF_ZONE_ID}" }, +] + +[[env.production.excludes]] +routes = [ + { pattern = "https://coryd.dev/books", zone_id = "${CF_ZONE_ID}" }, + { pattern = "https://coryd.dev/books/", zone_id = "${CF_ZONE_ID}" }, +] \ No newline at end of file