From aec8471b06ef77181259d84965f3561abb3daa4b Mon Sep 17 00:00:00 2001 From: Cory Dransfeldt <hi@coryd.dev> Date: Sun, 17 Nov 2024 11:55:53 -0800 Subject: [PATCH] chore: myriad fixes + book year pages --- _headers | 19 ++--- public/feeds/feed.xsl | 74 ++++++++++++++++++++ src/components/Footer.astro | 3 +- src/components/Header.astro | 3 +- src/components/Metadata.astro | 5 +- src/components/blocks/BlockRenderer.astro | 3 +- src/components/blocks/Hero.astro | 23 +++++- src/components/home/RecentPosts.astro | 2 +- src/components/media/Grid.astro | 3 +- src/components/media/music/Recent.astro | 4 +- src/components/media/watching/Hero.astro | 4 +- src/layouts/Layout.astro | 8 +-- src/pages/[permalink].astro | 4 +- src/pages/books/[isbn].astro | 5 +- src/pages/books/index.astro | 5 +- src/pages/books/years/[year].astro | 46 ++++++++++++ src/pages/feeds/{json => }/all.json.js | 15 ++-- src/pages/feeds/{rss => }/all.xml.js | 17 ++--- src/pages/feeds/books.json.js | 23 ++++++ src/pages/feeds/books.xml.js | 23 ++++++ src/pages/feeds/json/books.json.js | 22 ------ src/pages/feeds/json/links.json.js | 22 ------ src/pages/feeds/json/posts.json.js | 22 ------ src/pages/feeds/links.json.js | 23 ++++++ src/pages/feeds/links.xml.js | 23 ++++++ src/pages/feeds/{json => }/movies.json.js | 15 ++-- src/pages/feeds/movies.xml.js | 23 ++++++ src/pages/feeds/posts.json.js | 23 ++++++ src/pages/feeds/posts.xml.js | 23 ++++++ src/pages/feeds/rss/books.xml.js | 22 ------ src/pages/feeds/rss/links.xml.js | 22 ------ src/pages/feeds/rss/movies.xml.js | 22 ------ src/pages/feeds/rss/posts.xml.js | 22 ------ src/pages/feeds/rss/syndication.xml.js | 64 ----------------- src/pages/feeds/syndication.xml.js | 62 ++++++++++++++++ src/pages/index.astro | 5 +- src/pages/links.astro | 6 +- src/pages/posts/[...page].astro | 5 +- src/pages/posts/[year]/[title].astro | 5 +- src/utils/data/artists.js | 2 +- src/utils/data/{ => dynamic}/bookByUrl.js | 0 src/utils/data/global/index.js | 11 +++ src/utils/generateRssFeed.js | 10 +-- src/utils/{helpers.js => helpers/general.js} | 14 ++++ src/utils/helpers/media.js | 44 ++++++++++++ 45 files changed, 508 insertions(+), 293 deletions(-) create mode 100644 public/feeds/feed.xsl create mode 100644 src/pages/books/years/[year].astro rename src/pages/feeds/{json => }/all.json.js (60%) rename src/pages/feeds/{rss => }/all.xml.js (54%) create mode 100644 src/pages/feeds/books.json.js create mode 100644 src/pages/feeds/books.xml.js delete mode 100644 src/pages/feeds/json/books.json.js delete mode 100644 src/pages/feeds/json/links.json.js delete mode 100644 src/pages/feeds/json/posts.json.js create mode 100644 src/pages/feeds/links.json.js create mode 100644 src/pages/feeds/links.xml.js rename src/pages/feeds/{json => }/movies.json.js (59%) create mode 100644 src/pages/feeds/movies.xml.js create mode 100644 src/pages/feeds/posts.json.js create mode 100644 src/pages/feeds/posts.xml.js delete mode 100644 src/pages/feeds/rss/books.xml.js delete mode 100644 src/pages/feeds/rss/links.xml.js delete mode 100644 src/pages/feeds/rss/movies.xml.js delete mode 100644 src/pages/feeds/rss/posts.xml.js delete mode 100644 src/pages/feeds/rss/syndication.xml.js create mode 100644 src/pages/feeds/syndication.xml.js rename src/utils/data/{ => dynamic}/bookByUrl.js (100%) create mode 100644 src/utils/data/global/index.js rename src/utils/{helpers.js => helpers/general.js} (84%) create mode 100644 src/utils/helpers/media.js diff --git a/_headers b/_headers index d870296..586089b 100644 --- a/_headers +++ b/_headers @@ -1,46 +1,39 @@ -/feeds/album-releases - Content-Type: application/xml; charset=utf-8 - x-content-type-options: nosniff - -/feeds/album-releases.json - Content-Type: application/json - -/feeds/all +/feeds/all.xml Content-Type: application/xml; charset=utf-8 x-content-type-options: nosniff /feeds/all.json Content-Type: application/json -/feeds/books +/feeds/books.xml Content-Type: application/xml; charset=utf-8 x-content-type-options: nosniff /feeds/books.json Content-Type: application/json -/feeds/links +/feeds/links.xml Content-Type: application/xml; charset=utf-8 x-content-type-options: nosniff /feeds/links.json Content-Type: application/json -/feeds/posts +/feeds/posts.xml Content-Type: application/xml; charset=utf-8 x-content-type-options: nosniff /feeds/posts.json Content-Type: application/json -/feeds/movies +/feeds/movies.xml Content-Type: application/xml; charset=utf-8 x-content-type-options: nosniff /feeds/movies.json Content-Type: application/json -/feeds/syndication +/feeds/syndication.xml Content-Type: application/xml; charset=utf-8 x-content-type-options: nosniff diff --git a/public/feeds/feed.xsl b/public/feeds/feed.xsl new file mode 100644 index 0000000..f56741f --- /dev/null +++ b/public/feeds/feed.xsl @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="utf-8"?> +<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:atom="http://www.w3.org/2005/Atom"> + <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" /> + <xsl:template match="/"> + <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> + <head> + <title> + <xsl:value-of select="/rss/channel/title" /> + </title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> + <meta name="color-scheme" content="light dark" /> + <link rel="stylesheet" href="/assets/styles/index.css" type="text/css" /> + </head> + <body class="feed"> + <div class="main-wrapper"> + <main> + <section class="main-title"> + <h1> + <a href="/feeds" tabindex="0">Cory Dransfeldt</a> + </h1> + </section> + <div class="default-wrapper"> + <h2><xsl:value-of select="/rss/channel/title" /></h2> + <article class="intro"> + <p> + <xsl:value-of select="/rss/channel/description" /> + </p> + <p> + <strong class="highlight-text">Subscribe by adding the URL below to your feed reader + of choice.</strong> + </p> + <p> + <pre class="small"> + <code><xsl:value-of select="rss/channel/atom:link/@href"/></code> + </pre> + </p> + <p> + <a href="/feeds">View more of the feeds from my site.</a> + </p> + </article> + <section> + <xsl:for-each select="/rss/channel/item"> + <article> + <time>Published: <xsl:value-of select="pubDate" /></time> + <h3> + <a> + <xsl:attribute name="href"> + <xsl:value-of select="link" /> + </xsl:attribute> + <xsl:value-of select="title" /> + </a> + </h3> + <xsl:value-of select="description" disable-output-escaping="yes" /> + <xsl:if test="enclosure"> + <img class="image-banner" src="{enclosure/@url}" alt="{title}" /> + </xsl:if> + </article> + </xsl:for-each> + </section> + </div> + </main> + <footer> + <p>Subscribe by adding <code> + <xsl:value-of select="rss/channel/atom:link/@href" /> + </code> to your + feed reader of choice.</p> + </footer> + </div> + </body> + </html> + </xsl:template> +</xsl:stylesheet> \ No newline at end of file diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 0f9bfd4..b467210 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -1,8 +1,9 @@ --- import NavLink from '@components/nav/NavLink.astro'; +import { fetchGlobalData } from '@utils/data/global/index.js'; const { updated } = Astro.props; -const { nav } = Astro.locals; +const { nav } = await fetchGlobalData(Astro); --- <footer style={updated ? undefined : 'margin-top: var(--spacing-3xl)'}> diff --git a/src/components/Header.astro b/src/components/Header.astro index 44ba00e..3d7e809 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,8 +1,9 @@ --- import Menu from '@components/nav/Menu.astro'; +import { fetchGlobalData } from '@utils/data/global/index.js'; const { siteName, url } = Astro.props; -const { nav } = Astro.locals; +const { nav } = await fetchGlobalData(Astro); const isHomePage = url === '/'; --- diff --git a/src/components/Metadata.astro b/src/components/Metadata.astro index 4f70d52..f6830e3 100644 --- a/src/components/Metadata.astro +++ b/src/components/Metadata.astro @@ -1,5 +1,6 @@ --- -import { escapeHtml } from "@utils/helpers.js"; +import { escapeHtml } from "@utils/helpers/general.js"; +import { fetchGlobalData } from '@utils/data/global/index.js'; const { schema = "page", @@ -18,7 +19,7 @@ const { genre, year, } = Astro.props; -const { globals} = Astro.locals; +const { globals } = await fetchGlobalData(Astro); let pageTitle = globals.site_name; let pageDescription = globals.site_description; diff --git a/src/components/blocks/BlockRenderer.astro b/src/components/blocks/BlockRenderer.astro index c182293..02c0c35 100644 --- a/src/components/blocks/BlockRenderer.astro +++ b/src/components/blocks/BlockRenderer.astro @@ -12,14 +12,13 @@ import Npm from '@components/blocks/banners/Npm.astro'; import Rss from '@components/blocks/banners/Rss.astro'; import YouTubePlayer from '@components/blocks//YouTubePlayer.astro'; -import { md } from '@utils/helpers.js'; +import { md } from '@utils/helpers/general.js'; import { getPopularPosts } from '@utils/getPopularPosts.js'; const analytics = await fetchAnalyticsData(); const links = await fetchLinks(); const posts = await fetchAllPosts(); const popularPosts = getPopularPosts(posts, analytics); - const { block } = Astro.props; --- diff --git a/src/components/blocks/Hero.astro b/src/components/blocks/Hero.astro index 7f32239..38d040a 100644 --- a/src/components/blocks/Hero.astro +++ b/src/components/blocks/Hero.astro @@ -1,7 +1,26 @@ --- +import { fetchGlobals } from "@utils/data/globals.js"; + const { image, alt } = Astro.props; +const globals = await fetchGlobals(Astro); --- <div class="hero"> - <img src={image} alt={alt} /> -</div> \ No newline at end of file + <img + srcset={` + ${globals.cdn_url}${image}?class=bannersm&type=webp 256w, + ${globals.cdn_url}${image}?class=bannermd&type=webp 512w, + ${globals.cdn_url}${image}?class=bannerbase&type=webp 1024w + `} + sizes="(max-width: 450px) 256px, + (max-width: 850px) 512px, + 1024px" + src={`${globals.cdn_url}${image}?class=bannersm&type=webp`} + alt={alt} + class="image-banner" + loading="lazy" + decoding="async" + width="720" + height="480" + /> +</div> diff --git a/src/components/home/RecentPosts.astro b/src/components/home/RecentPosts.astro index a222d3d..32be80c 100644 --- a/src/components/home/RecentPosts.astro +++ b/src/components/home/RecentPosts.astro @@ -1,7 +1,7 @@ --- import { IconClock, IconStar, IconArrowRight } from '@tabler/icons-react'; import { fetchAllPosts } from '@utils/data/posts.js'; -import { md } from '@utils/helpers.js'; +import { md } from '@utils/helpers/general.js'; const posts = await fetchAllPosts(); --- diff --git a/src/components/media/Grid.astro b/src/components/media/Grid.astro index 1842773..1d4bc3c 100644 --- a/src/components/media/Grid.astro +++ b/src/components/media/Grid.astro @@ -1,8 +1,9 @@ --- import Paginator from '@components/nav/Paginator.astro'; +import { fetchGlobalData } from '@utils/data/global/index.js'; const { data, count, shape, pagination, loading = "lazy" } = Astro.props; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const pageCount = pagination?.pages?.length || 0; const hidePagination = pageCount <= 1; diff --git a/src/components/media/music/Recent.astro b/src/components/media/music/Recent.astro index 5cc0e3a..576d5e1 100644 --- a/src/components/media/music/Recent.astro +++ b/src/components/media/music/Recent.astro @@ -1,6 +1,8 @@ --- +import { fetchGlobalData } from '@utils/data/global/index.js'; + const { data } = Astro.props; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); --- <div class="music-chart"> diff --git a/src/components/media/watching/Hero.astro b/src/components/media/watching/Hero.astro index fa72d4c..ce972dd 100644 --- a/src/components/media/watching/Hero.astro +++ b/src/components/media/watching/Hero.astro @@ -1,8 +1,8 @@ --- +import { fetchGlobalData } from '@utils/data/global/index.js'; import Hero from "@components/blocks/Hero.astro"; const { movie } = Astro.props; -const { globals } = Astro.locals; --- <a href={movie.url}> @@ -14,6 +14,6 @@ const { globals } = Astro.locals; ({movie.year}) </div> </div> - <Hero globals={globals} image={movie.backdrop} alt={movie.title} /> + <Hero image={movie.backdrop} alt={movie.title} /> </div> </a> diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 734f023..d8966fd 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -3,10 +3,7 @@ import "@styles/index.css"; import Header from "@components/Header.astro"; import Footer from "@components/Footer.astro"; import Metadata from "@components/Metadata.astro"; -import { fetchNavigation } from "@utils/data/nav.js"; - -const currentUrl = Astro.url.pathname; -const nav = await fetchNavigation(); +import { fetchGlobalData } from '@utils/data/global/index.js'; const { schema = "page", @@ -17,7 +14,8 @@ const { updated, ...otherProps } = Astro.props; -const { globals} = Astro.locals; +const { globals } = await fetchGlobalData(Astro); +const currentUrl = Astro.url.pathname; const isProduction = import.meta.env.MODE === "production"; --- diff --git a/src/pages/[permalink].astro b/src/pages/[permalink].astro index 6379606..2479542 100644 --- a/src/pages/[permalink].astro +++ b/src/pages/[permalink].astro @@ -2,6 +2,7 @@ import Layout from '@layouts/Layout.astro'; import BlockRenderer from '@components/blocks/BlockRenderer.astro'; import { fetchPages } from '@utils/data/pages'; +import { fetchGlobalData } from '@utils/data/global/index.js'; export const prerender = true; @@ -14,12 +15,11 @@ export async function getStaticPaths() { } const { page } = Astro.props; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const currentUrl = Astro.url.pathname; --- <Layout - globals={globals} pageTitle={page.title} description={page.description} ogImage={page.open_graph_image} diff --git a/src/pages/books/[isbn].astro b/src/pages/books/[isbn].astro index 32ab6ac..7d6e566 100644 --- a/src/pages/books/[isbn].astro +++ b/src/pages/books/[isbn].astro @@ -4,7 +4,8 @@ import Warning from "@components/blocks/banners/Warning.astro"; import AssociatedMedia from "@components/blocks/AssociatedMedia.astro"; import ProgressBar from "@components/media/ProgressBar.astro"; import { IconArrowLeft, IconHeart, IconNeedle } from "@tabler/icons-react"; -import { fetchBookByUrl } from "@utils/data/bookByUrl.js"; +import { fetchBookByUrl } from "@utils/data/dynamic/bookByUrl.js"; +import { fetchGlobalData } from '@utils/data/global/index.js'; const { isbn } = Astro.params; @@ -14,7 +15,7 @@ if (!book) return Astro.redirect("/404", 404); const alt = `${book.title}${book.author ? ` by ${book.author}` : ""}`; const pageTitle = `Books / ${book.title}`; const description = book.description || `Details about the book ${book.title}`; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); --- <Layout pageTitle={pageTitle} description={description} schema="book"> diff --git a/src/pages/books/index.astro b/src/pages/books/index.astro index 3d85bdd..2d78809 100644 --- a/src/pages/books/index.astro +++ b/src/pages/books/index.astro @@ -3,10 +3,11 @@ import Layout from "@layouts/Layout.astro"; import Rss from "@components/blocks/banners/Rss.astro"; import ProgressBar from "@components/media/ProgressBar.astro"; import { fetchBooks } from "@utils/data/books.js"; -import { md, htmlTruncate } from "@utils/helpers.js"; +import { fetchGlobalData } from '@utils/data/global/index.js'; +import { md, htmlTruncate } from "@utils/helpers/general.js"; const books = await fetchBooks(); -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const title = "Currently reading"; const description = "Here's what I'm reading at the moment."; const updated = new Date().toISOString(); diff --git a/src/pages/books/years/[year].astro b/src/pages/books/years/[year].astro new file mode 100644 index 0000000..54b2321 --- /dev/null +++ b/src/pages/books/years/[year].astro @@ -0,0 +1,46 @@ +--- +import Layout from "@layouts/Layout.astro"; +import Grid from "@components/media/Grid.astro"; +import { IconArrowLeft } from "@tabler/icons-react"; +import { filterBooksByStatus, findFavoriteBooks, mediaLinks } from "@utils/helpers/media.js"; +import { fetchGlobalData } from '@utils/data/global/index.js'; +import { fetchBooks } from "@utils/data/books.js"; +import { DateTime } from "luxon"; + +const { globals } = await fetchGlobalData(Astro); +const books = await fetchBooks(); +const { year } = Astro.params; +const yearData = books.years.find((y) => y.value === parseInt(year, 10)); + +if (!yearData) return Astro.redirect("/404", 404); + +const bookData = filterBooksByStatus(yearData.data, "finished"); +const bookDataFavorites = findFavoriteBooks(bookData); +const favoriteBooks = mediaLinks(bookDataFavorites, "book", 5); +const currentYear = DateTime.now().year; +const isCurrentYear = parseInt(year, 10) === currentYear; +const pageTitle = `${year} / Books`; +const description = isCurrentYear + ? `I've finished ${bookData.length} books this year.` + : `I finished ${bookData.length} books in ${year}.`; +const intro = isCurrentYear + ? ` + I've finished <strong class="highlight-text">${bookData.length} books</strong> this year. + ${favoriteBooks ? ` Among my favorites are ${favoriteBooks}.` : ''} + ` + : ` + I finished <strong class="highlight-text">${bookData.length} books</strong> in + <strong class="highlight-text">${year}</strong>. + ${favoriteBooks ? ` Among my favorites were ${favoriteBooks}.` : ''} + `; +--- + +<Layout globals={globals} pageTitle={pageTitle} description={description} schema="books-year"> + <a href="/books" class="back-link"> + <IconArrowLeft size={18} /> Back to books + </a> + <h2 class="page-title">{year} / Books</h2> + <div set:html={intro}></div> + <hr /> + <Grid globals={globals} data={bookData} shape="vertical" count={200} loading="eager" /> +</Layout> diff --git a/src/pages/feeds/json/all.json.js b/src/pages/feeds/all.json.js similarity index 60% rename from src/pages/feeds/json/all.json.js rename to src/pages/feeds/all.json.js index 6cc1a4d..3c02bc7 100644 --- a/src/pages/feeds/json/all.json.js +++ b/src/pages/feeds/all.json.js @@ -1,8 +1,10 @@ import { generateJsonFeed } from '@utils/generateJsonFeed'; import { fetchGlobals } from '@utils/data/globals'; import { fetchActivity } from '@utils/data/activity'; +import fs from 'fs/promises'; +import path from 'path'; -export async function GET() { +export async function getStaticPaths() { const globals = await fetchGlobals(); const activity = await fetchActivity(); @@ -13,10 +15,9 @@ export async function GET() { data: activity, }); - return new Response(feed, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); + const filePath = path.resolve('public/feeds/all.json'); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, feed); + + return []; } diff --git a/src/pages/feeds/rss/all.xml.js b/src/pages/feeds/all.xml.js similarity index 54% rename from src/pages/feeds/rss/all.xml.js rename to src/pages/feeds/all.xml.js index 77bac08..bc37160 100644 --- a/src/pages/feeds/rss/all.xml.js +++ b/src/pages/feeds/all.xml.js @@ -1,22 +1,23 @@ import { generateRssFeed } from "@utils/generateRssFeed"; import { fetchGlobals } from "@utils/data/globals"; import { fetchActivity } from "@utils/data/activity"; +import fs from "fs/promises"; +import path from "path"; -export async function GET() { +export async function getStaticPaths() { const globals = await fetchGlobals(); const activity = await fetchActivity(); const rss = generateRssFeed({ permalink: "/feeds/all.xml", - title: "All activity / Cory Dransfeldt", + title: "All activity feed", globals, data: activity, }); - return new Response(rss, { - status: 200, - headers: { - "Content-Type": "application/rss+xml", - }, - }); + const filePath = path.resolve("public/feeds/all.xml"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, rss); + + return []; } diff --git a/src/pages/feeds/books.json.js b/src/pages/feeds/books.json.js new file mode 100644 index 0000000..6d6838d --- /dev/null +++ b/src/pages/feeds/books.json.js @@ -0,0 +1,23 @@ +import { generateJsonFeed } from "@utils/generateJsonFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchBooks } from "@utils/data/books"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const books = await fetchBooks(); + + const feed = generateJsonFeed({ + permalink: "/feeds/books.json", + title: "Books / Cory Dransfeldt", + globals, + data: books.feed, + }); + + const filePath = path.resolve("public/feeds/books.json"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, feed); + + return []; +} diff --git a/src/pages/feeds/books.xml.js b/src/pages/feeds/books.xml.js new file mode 100644 index 0000000..955feed --- /dev/null +++ b/src/pages/feeds/books.xml.js @@ -0,0 +1,23 @@ +import { generateRssFeed } from "@utils/generateRssFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchBooks } from "@utils/data/books"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const books = await fetchBooks(); + + const rss = generateRssFeed({ + permalink: "/feeds/books.xml", + title: "Books feed", + globals, + data: books.feed, + }); + + const filePath = path.resolve("public/feeds/books.xml"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, rss); + + return []; +} diff --git a/src/pages/feeds/json/books.json.js b/src/pages/feeds/json/books.json.js deleted file mode 100644 index 54e2a61..0000000 --- a/src/pages/feeds/json/books.json.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateJsonFeed } from '@utils/generateJsonFeed'; -import { fetchGlobals } from '@utils/data/globals'; -import { fetchBooks } from '@utils/data/books'; - -export async function GET() { - const globals = await fetchGlobals(); - const books = await fetchBooks(); - - const feed = generateJsonFeed({ - permalink: "/feeds/books.json", - title: "Books / Cory Dransfeldt", - globals, - data: books.feed, - }); - - return new Response(feed, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); -} diff --git a/src/pages/feeds/json/links.json.js b/src/pages/feeds/json/links.json.js deleted file mode 100644 index 98fdd5f..0000000 --- a/src/pages/feeds/json/links.json.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateJsonFeed } from '@utils/generateJsonFeed'; -import { fetchGlobals } from '@utils/data/globals'; -import { fetchLinks } from '@utils/data/links'; - -export async function GET() { - const globals = await fetchGlobals(); - const links = await fetchLinks(); - - const feed = generateJsonFeed({ - permalink: "/feeds/links.json", - title: "Links / Cory Dransfeldt", - globals, - data: links, - }); - - return new Response(feed, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); -} diff --git a/src/pages/feeds/json/posts.json.js b/src/pages/feeds/json/posts.json.js deleted file mode 100644 index ea6e978..0000000 --- a/src/pages/feeds/json/posts.json.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateJsonFeed } from '@utils/generateJsonFeed'; -import { fetchGlobals } from '@utils/data/globals'; -import { fetchAllPosts } from '@utils/data/posts'; - -export async function GET() { - const globals = await fetchGlobals(); - const posts = await fetchAllPosts(); - - const feed = generateJsonFeed({ - permalink: "/feeds/posts.json", - title: "Posts / Cory Dransfeldt", - globals, - data: posts, - }); - - return new Response(feed, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); -} diff --git a/src/pages/feeds/links.json.js b/src/pages/feeds/links.json.js new file mode 100644 index 0000000..a84243e --- /dev/null +++ b/src/pages/feeds/links.json.js @@ -0,0 +1,23 @@ +import { generateJsonFeed } from "@utils/generateJsonFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchLinks } from "@utils/data/links"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const links = await fetchLinks(); + + const feed = generateJsonFeed({ + permalink: "/feeds/links.json", + title: "Links / Cory Dransfeldt", + globals, + data: links, + }); + + const filePath = path.resolve("public/feeds/links.json"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, feed); + + return []; +} diff --git a/src/pages/feeds/links.xml.js b/src/pages/feeds/links.xml.js new file mode 100644 index 0000000..57913a5 --- /dev/null +++ b/src/pages/feeds/links.xml.js @@ -0,0 +1,23 @@ +import { generateRssFeed } from "@utils/generateRssFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchLinks } from "@utils/data/links"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const links = await fetchLinks(); + + const rss = generateRssFeed({ + permalink: "/feeds/links.xml", + title: "Links feed", + globals, + data: links, + }); + + const filePath = path.resolve("public/feeds/links.xml"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, rss); + + return []; +} diff --git a/src/pages/feeds/json/movies.json.js b/src/pages/feeds/movies.json.js similarity index 59% rename from src/pages/feeds/json/movies.json.js rename to src/pages/feeds/movies.json.js index 852eeea..bebd3ee 100644 --- a/src/pages/feeds/json/movies.json.js +++ b/src/pages/feeds/movies.json.js @@ -1,8 +1,10 @@ import { generateJsonFeed } from '@utils/generateJsonFeed'; import { fetchGlobals } from '@utils/data/globals'; import { fetchMovies } from '@utils/data/movies'; +import fs from 'fs/promises'; +import path from 'path'; -export async function GET() { +export async function getStaticPaths() { const globals = await fetchGlobals(); const movies = await fetchMovies(); @@ -13,10 +15,9 @@ export async function GET() { data: movies.feed, }); - return new Response(feed, { - status: 200, - headers: { - "Content-Type": "application/json", - }, - }); + const filePath = path.resolve("public/feeds/movies.json"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, feed); + + return []; } diff --git a/src/pages/feeds/movies.xml.js b/src/pages/feeds/movies.xml.js new file mode 100644 index 0000000..688365a --- /dev/null +++ b/src/pages/feeds/movies.xml.js @@ -0,0 +1,23 @@ +import { generateRssFeed } from "@utils/generateRssFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchMovies } from "@utils/data/movies"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const movies = await fetchMovies(); + + const rss = generateRssFeed({ + permalink: "/feeds/movies.xml", + title: "Movies feed", + globals, + data: movies.feed, + }); + + const filePath = path.resolve("public/feeds/movies.xml"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, rss); + + return []; +} diff --git a/src/pages/feeds/posts.json.js b/src/pages/feeds/posts.json.js new file mode 100644 index 0000000..50d9424 --- /dev/null +++ b/src/pages/feeds/posts.json.js @@ -0,0 +1,23 @@ +import { generateJsonFeed } from "@utils/generateJsonFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchAllPosts } from "@utils/data/posts"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const posts = await fetchAllPosts(); + + const feed = generateJsonFeed({ + permalink: "/feeds/posts.json", + title: "Posts / Cory Dransfeldt", + globals, + data: posts, + }); + + const filePath = path.resolve("public/feeds/posts.json"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, feed); + + return []; +} diff --git a/src/pages/feeds/posts.xml.js b/src/pages/feeds/posts.xml.js new file mode 100644 index 0000000..84ac3c1 --- /dev/null +++ b/src/pages/feeds/posts.xml.js @@ -0,0 +1,23 @@ +import { generateRssFeed } from "@utils/generateRssFeed"; +import { fetchGlobals } from "@utils/data/globals"; +import { fetchAllPosts } from "@utils/data/posts"; +import fs from "fs/promises"; +import path from "path"; + +export async function getStaticPaths() { + const globals = await fetchGlobals(); + const posts = await fetchAllPosts(); + + const rss = generateRssFeed({ + permalink: "/feeds/posts.xml", + title: "Posts feed", + globals, + data: posts, + }); + + const filePath = path.resolve("public/feeds/posts.xml"); + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, rss); + + return []; +} diff --git a/src/pages/feeds/rss/books.xml.js b/src/pages/feeds/rss/books.xml.js deleted file mode 100644 index 187c346..0000000 --- a/src/pages/feeds/rss/books.xml.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateRssFeed } from "@utils/generateRssFeed"; -import { fetchGlobals } from "@utils/data/globals"; -import { fetchBooks } from '@utils/data/books'; - -export async function GET() { - const globals = await fetchGlobals(); - const books = await fetchBooks(); - - const rss = generateRssFeed({ - permalink: "/feeds/books.xml", - title: "Books / Cory Dransfeldt", - globals, - data: books.feed, - }); - - return new Response(rss, { - status: 200, - headers: { - "Content-Type": "application/rss+xml", - }, - }); -} diff --git a/src/pages/feeds/rss/links.xml.js b/src/pages/feeds/rss/links.xml.js deleted file mode 100644 index e2219cd..0000000 --- a/src/pages/feeds/rss/links.xml.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateRssFeed } from "@utils/generateRssFeed"; -import { fetchGlobals } from "@utils/data/globals"; -import { fetchLinks } from '@utils/data/links'; - -export async function GET() { - const globals = await fetchGlobals(); - const links = await fetchLinks(); - - const rss = generateRssFeed({ - permalink: "/feeds/links.xml", - title: "Links / Cory Dransfeldt", - globals, - data: links, - }); - - return new Response(rss, { - status: 200, - headers: { - "Content-Type": "application/rss+xml", - }, - }); -} diff --git a/src/pages/feeds/rss/movies.xml.js b/src/pages/feeds/rss/movies.xml.js deleted file mode 100644 index 78a314d..0000000 --- a/src/pages/feeds/rss/movies.xml.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateRssFeed } from "@utils/generateRssFeed"; -import { fetchGlobals } from "@utils/data/globals"; -import { fetchMovies } from '@utils/data/movies'; - -export async function GET() { - const globals = await fetchGlobals(); - const movies = await fetchMovies(); - - const rss = generateRssFeed({ - permalink: "/feeds/movies.xml", - title: "Movies / Cory Dransfeldt", - globals, - data: movies.feed, - }); - - return new Response(rss, { - status: 200, - headers: { - "Content-Type": "application/rss+xml", - }, - }); -} diff --git a/src/pages/feeds/rss/posts.xml.js b/src/pages/feeds/rss/posts.xml.js deleted file mode 100644 index 53f9890..0000000 --- a/src/pages/feeds/rss/posts.xml.js +++ /dev/null @@ -1,22 +0,0 @@ -import { generateRssFeed } from "@utils/generateRssFeed"; -import { fetchGlobals } from "@utils/data/globals"; -import { fetchAllPosts } from '@utils/data/posts'; - -export async function GET() { - const globals = await fetchGlobals(); - const posts = await fetchAllPosts(); - - const rss = generateRssFeed({ - permalink: "/feeds/posts.xml", - title: "Posts / Cory Dransfeldt", - globals, - data: posts, - }); - - return new Response(rss, { - status: 200, - headers: { - "Content-Type": "application/rss+xml", - }, - }); -} diff --git a/src/pages/feeds/rss/syndication.xml.js b/src/pages/feeds/rss/syndication.xml.js deleted file mode 100644 index b75af08..0000000 --- a/src/pages/feeds/rss/syndication.xml.js +++ /dev/null @@ -1,64 +0,0 @@ -import fetchSyndication from '@utils/data/syndication.js'; -import { fetchGlobals } from '@utils/data/globals.js'; - -export async function GET() { - const globals = await fetchGlobals(); - const entries = await fetchSyndication(); - - if (!entries.length) return new Response('No feed entries found.', { status: 404 }); - - const title = globals.site_name || 'Syndicated content / Cory Dransfeldt'; - const permalink = '/feeds/syndication.xml'; - - const xml = `<?xml version="1.0" encoding="UTF-8" ?> -<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> - <channel> - <atom:link href="${globals.url}${permalink}" rel="self" type="application/rss+xml" /> - <title><![CDATA[${title}]]></title> - <description><![CDATA[${globals.site_description || ''}]]></description> - <link>${globals.url}${permalink}</link> - <lastBuildDate>${new Date().toUTCString()}</lastBuildDate> - <image> - <title><![CDATA[${title}]]></title> - <link>${globals.url}${permalink}</link> - <url>${globals.cdn_url}${globals.avatar}?class=w200</url> - <width>144</width> - <height>144</height> - </image> - ${entries - .slice(0, 20) - .map( - (entry) => ` - <item> - <title><![CDATA[${entry.syndication.title}]]></title> - <link>${encodeAmp(entry.syndication.url)}</link> - <pubDate>${new Date(entry.syndication.date).toUTCString()}</pubDate> - <guid isPermaLink="false">${encodeAmp(entry.syndication.url)}</guid> - <description><![CDATA[${escapeHTML(entry.syndication.description)}]]></description> - </item>` - ) - .join('')} - </channel> -</rss>`; - - return new Response(xml, { - status: 200, - headers: { - 'Content-Type': 'application/rss+xml', - }, - }); -} - -function encodeAmp(url) { - return url.replace(/&/g, '&'); -} - -function escapeHTML(str) { - if (!str) return ''; - return str - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} \ No newline at end of file diff --git a/src/pages/feeds/syndication.xml.js b/src/pages/feeds/syndication.xml.js new file mode 100644 index 0000000..4135729 --- /dev/null +++ b/src/pages/feeds/syndication.xml.js @@ -0,0 +1,62 @@ +import { DateTime } from "luxon"; +import fetchSyndication from '@utils/data/syndication.js'; +import { fetchGlobals } from '@utils/data/globals.js'; +import { dateToRFC822, encodeAmp, md } from '@utils/helpers/general.js'; + +const generateSyndicationRSS = async () => { + const globals = await fetchGlobals(); + const entries = await fetchSyndication(); + + if (!entries.length) throw new Error('No feed entries found.'); + + const title = globals.site_name || 'Syndicated Content Feed'; + const permalink = '/feeds/syndication.xml'; + + const xml = `<?xml version="1.0" encoding="UTF-8"?> +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> + <channel> + <atom:link href="${globals.url}${permalink}" rel="self" type="application/rss+xml" /> + <title><![CDATA[${title}]]></title> + <description><![CDATA[${globals.site_description || ''}]]></description> + <link>${globals.url}</link> + <lastBuildDate>${dateToRFC822(DateTime.now())}</lastBuildDate> + <image> + <title><![CDATA[${title}]]></title> + <link>${globals.url}</link> + <url>${globals.cdn_url}${globals.avatar}?class=w200</url> + <width>144</width> + <height>144</height> + </image> + ${entries + .slice(0, 20) + .map( + (entry) => ` + <item> + <title><![CDATA[${entry.syndication.title || 'Untitled'}]]></title> + <link>${encodeAmp(entry.syndication.url)}</link> + <pubDate>${dateToRFC822(entry.syndication.date)}</pubDate> + <guid isPermaLink="false">${encodeAmp(entry.syndication.url)}</guid> + <description><![CDATA[${md(entry.syndication.description || '')}]]></description> + </item>` + ) + .join('')} + </channel> +</rss>`; + + return xml; +}; + +export async function GET() { + try { + const rss = await generateSyndicationRSS(); + return new Response(rss, { + status: 200, + headers: { 'Content-Type': 'application/rss+xml' }, + }); + } catch (error) { + console.error(error.message); + return new Response('Error generating syndication feed.', { status: 500 }); + } +} + +export const prerender = true; diff --git a/src/pages/index.astro b/src/pages/index.astro index fae8b66..d21e416 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,8 +3,9 @@ import Layout from '@layouts/Layout.astro'; import Intro from '@components/home/Intro.astro'; import RecentActivity from '@components/home/RecentActivity.astro'; import RecentPosts from '@components/home/RecentPosts.astro'; +import { fetchGlobalData } from '@utils/data/global/index.js'; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const schema = 'blog'; const pageTitle = globals.site_name; const description = 'This is a blog post description'; @@ -13,14 +14,12 @@ const fullUrl = globals.url + '/blog/my-post'; const themeColor = globals.theme_color; --- <Layout - globals={globals} pageTitle={pageTitle} description={description} ogImage={ogImage} fullUrl={fullUrl} themeColor={themeColor} schema={schema} - globals={globals} > <Intro intro={globals.intro} /> <RecentActivity /> diff --git a/src/pages/links.astro b/src/pages/links.astro index 1965349..38fcb52 100644 --- a/src/pages/links.astro +++ b/src/pages/links.astro @@ -3,14 +3,12 @@ import Layout from "@layouts/Layout.astro"; import Paginator from "@components/nav/Paginator.astro"; import RssBanner from "@components/blocks/banners/Rss.astro"; import { fetchLinks } from "@utils/data/links.js"; +import { fetchGlobalData } from '@utils/data/global/index.js'; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const links = await fetchLinks(); - const title = "Links"; const description = "These are links I've liked or otherwise found interesting. They're all added manually, after having been read and, I suppose, properly considered."; - -// Pagination Settings const pageSize = 30; const currentPage = parseInt(Astro.url.searchParams.get("page") || "1", 10); const totalPages = Math.ceil(links.length / pageSize); diff --git a/src/pages/posts/[...page].astro b/src/pages/posts/[...page].astro index a2dc7ba..28343fa 100644 --- a/src/pages/posts/[...page].astro +++ b/src/pages/posts/[...page].astro @@ -2,14 +2,15 @@ import { getCollection } from 'astro:content'; import { IconStar } from '@tabler/icons-react'; import { fetchAllPosts } from "@data/posts.js"; +import { fetchGlobalData } from '@utils/data/global/index.js'; import Layout from "@layouts/Layout.astro"; import Paginator from '@components/nav/Paginator.astro'; -import { md } from '@utils/helpers.js'; +import { md } from '@utils/helpers/general.js'; import { DateTime } from 'luxon'; const posts = await fetchAllPosts(); const { page } = Astro.props; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const currentUrl = Astro.url.pathname; const currentPage = Astro.params.page ? parseInt(Astro.params.page, 10) : 1; diff --git a/src/pages/posts/[year]/[title].astro b/src/pages/posts/[year]/[title].astro index 566ee50..6fc69cf 100644 --- a/src/pages/posts/[year]/[title].astro +++ b/src/pages/posts/[year]/[title].astro @@ -3,7 +3,8 @@ import { IconStar } from "@tabler/icons-react"; import { fetchAllPosts } from "@data/posts.js"; import { fetchAnalyticsData } from "@data/analytics.js"; import { fetchLinks } from "@data/links.js"; -import { md } from '@utils/helpers.js'; +import { fetchGlobalData } from '@utils/data/global/index.js'; +import { md } from '@utils/helpers/general.js'; import { getPopularPosts } from '@utils/getPopularPosts.js'; const analytics = await fetchAnalyticsData(); @@ -38,7 +39,7 @@ export async function getStaticPaths() { } const { post } = Astro.props; -const { globals } = Astro.locals; +const { globals } = await fetchGlobalData(Astro); const { year, title } = Astro.params; const currentUrl = Astro.url.pathname; const htmlContent = md(post.content); diff --git a/src/utils/data/artists.js b/src/utils/data/artists.js index b5482dc..fdcc344 100644 --- a/src/utils/data/artists.js +++ b/src/utils/data/artists.js @@ -1,5 +1,5 @@ import { createClient } from "@supabase/supabase-js"; -import { parseCountryField } from "@utils/helpers.js"; +import { parseCountryField } from "@utils/helpers/general.js"; const SUPABASE_URL = import.meta.env.SUPABASE_URL; const SUPABASE_KEY = import.meta.env.SUPABASE_KEY; diff --git a/src/utils/data/bookByUrl.js b/src/utils/data/dynamic/bookByUrl.js similarity index 100% rename from src/utils/data/bookByUrl.js rename to src/utils/data/dynamic/bookByUrl.js diff --git a/src/utils/data/global/index.js b/src/utils/data/global/index.js new file mode 100644 index 0000000..0f2277b --- /dev/null +++ b/src/utils/data/global/index.js @@ -0,0 +1,11 @@ +import { fetchGlobals } from "@utils/data/globals.js"; +import { fetchNavigation } from "@utils/data/nav.js"; + +export async function fetchGlobalData(Astro) { + if (Astro?.locals) return Astro.locals; + + const globals = await fetchGlobals(); + const nav = await fetchNavigation(); + + return { globals, nav }; +} diff --git a/src/utils/generateRssFeed.js b/src/utils/generateRssFeed.js index f0c347f..e156f8c 100644 --- a/src/utils/generateRssFeed.js +++ b/src/utils/generateRssFeed.js @@ -1,4 +1,5 @@ import { DateTime } from "luxon"; +import { dateToRFC822, encodeAmp, md } from '@utils/helpers/general.js'; export function generateRssFeed({ permalink, title, globals, data }) { const rssItems = data.slice(0, 20).map((entry) => { @@ -11,27 +12,28 @@ export function generateRssFeed({ permalink, title, globals, data }) { return ` <item> <title><![CDATA[${entryTitle}]]></title> - <link>${entryFeed.url}</link> - <pubDate>${DateTime.fromISO(entryFeed.date).toRFC2822()}</pubDate> + <link>${encodeAmp(entryFeed.url)}</link> + <pubDate>${dateToRFC822(entryFeed.date)}</pubDate> <guid isPermaLink="false">${entryFeed.url}</guid> ${ entryFeed.image ? `<enclosure url="${globals.cdn_url}${entryFeed.image}?class=w800&type=jpg" type="image/jpeg" />` : "" } - <description><![CDATA[${entryFeed.description}]]></description> + <description><![CDATA[${md(entryFeed.description)}]]></description> </item>`; }); return ` <?xml version="1.0" encoding="UTF-8" ?> +<?xml-stylesheet href="/feeds/feed.xsl" type="text/xsl" ?> <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <atom:link href="${globals.url}${permalink}" rel="self" type="application/rss+xml" /> <title><![CDATA[${title}]]></title> <description><![CDATA[${globals.site_description}]]></description> <link>${globals.url}${permalink}</link> - <lastBuildDate>${DateTime.now().toUTC().toRFC2822()}</lastBuildDate> + <lastBuildDate>${dateToRFC822(DateTime.now())}</lastBuildDate> <image> <title><![CDATA[${title}]]></title> <link>${globals.url}${permalink}</link> diff --git a/src/utils/helpers.js b/src/utils/helpers/general.js similarity index 84% rename from src/utils/helpers.js rename to src/utils/helpers/general.js index 97252f3..14d3795 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers/general.js @@ -1,3 +1,4 @@ +import { DateTime } from "luxon"; import markdownIt from "markdown-it"; import markdownItAnchor from "markdown-it-anchor"; import markdownItFootnote from "markdown-it-footnote"; @@ -14,6 +15,7 @@ markdown.use(markdownItAnchor, { markdown.use(markdownItFootnote); markdown.use(markdownItPrism); +// arrays export const shuffleArray = (array) => { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { @@ -25,6 +27,7 @@ export const shuffleArray = (array) => { return shuffled; }; +// countries export const regionNames = new Intl.DisplayNames(["en"], { type: "region" }); export const getCountryName = (countryCode) => @@ -43,8 +46,10 @@ export const parseCountryField = (countryField) => { return countries.map(getCountryName).join(", "); }; +// markdown export const md = (string) => markdown.render(string); +// html export const htmlTruncate = (content, limit = 50) => truncateHtml(content, limit, { byWords: true, @@ -63,3 +68,12 @@ export const escapeHtml = (str) => ">": ">", }[char] || char) ); + +// urls +export const encodeAmp = (url) => url.replace(/&/g, "&"); + +// dates +export const dateToRFC822 = (date) => + DateTime.fromJSDate(date, { zone: "America/Los_Angeles" }).toFormat( + "ccc, dd LLL yyyy HH:mm:ss ZZZ" + ); diff --git a/src/utils/helpers/media.js b/src/utils/helpers/media.js new file mode 100644 index 0000000..996a9b8 --- /dev/null +++ b/src/utils/helpers/media.js @@ -0,0 +1,44 @@ +export const filterBooksByStatus = (books, status) => + books.filter((book) => book["status"] === status); + +export const findFavoriteBooks = (books) => + books.filter((book) => book["favorite"] === true); + +export const bookYearLinks = (years) => + years + .sort((a, b) => b["value"] - a["value"]) + .map( + (year, index) => + `<a href="/books/years/${year["value"]}">${year["value"]}</a>${ + index < years.length - 1 ? " / " : "" + }` + ) + .join(""); + +export const mediaLinks = (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 `<a href="${item["genre_url"]}">${item["genre_name"]}</a>`; + case "artist": + return `<a href="${item["url"]}">${item["name"]}</a>`; + case "book": + return `<a href="${item["url"]}">${item["title"]}</a>`; + 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}`; +};