feat: static pages

This commit is contained in:
Cory Dransfeldt 2024-11-11 08:59:00 -08:00
parent 96bff400e8
commit 5f43e417d1
No known key found for this signature in database
34 changed files with 409 additions and 1567 deletions

View file

@ -17,18 +17,16 @@ function sanitizeMediaString(str) {
export default {
async fetch(request, env) {
const directusUrl = env["DIRECTUS_URL"];
const directusToken = env["DIRECTUS_API_TOKEN"];
const artistImportToken = env["ARTIST_IMPORT_TOKEN"];
const artistFlowID = env["ARTIST_FLOW_ID"];
const albumFlowID = env["ALBUM_FLOW_ID"];
const directusUrl = env.DIRECTUS_URL;
const directusToken = env.DIRECTUS_API_TOKEN;
const artistImportToken = env.ARTIST_IMPORT_TOKEN;
const artistFlowID = env.ARTIST_FLOW_ID;
const albumFlowID = env.ALBUM_FLOW_ID;
const placeholderImageId = "4cef75db-831f-4f5d-9333-79eaa5bb55ee";
const requestUrl = new URL(request["url"]);
const providedToken = requestUrl.searchParams.get("token");
if (!providedToken || providedToken !== artistImportToken) {
return new Response("Unauthorized", { status: 401 });
}
if (!providedToken || providedToken !== artistImportToken) return new Response("Unauthorized", { status: 401 });
async function saveToDirectus(endpoint, payload) {
const response = await fetch(`${directusUrl}/items/${endpoint}`, {

View file

@ -36,8 +36,8 @@ export default {
return new Response("Invalid input", { status: 400 });
const emailDomain = email.split("@")[1].toLowerCase();
const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY;
const supabaseUrl = env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: blockedDomains, error: domainError } = await supabase
.from("blocked_domains")

View file

@ -1,80 +0,0 @@
import { createClient } from "@supabase/supabase-js";
import { fetchDataByUrl, fetchGlobals } from "./utils/fetchers.js";
import { minifyHTML } from "./utils/formatters.js";
import {
generateArtistHTML,
generateBookHTML,
generateGenreHTML,
generateMetadata,
generateWatchingHTML,
} from "./utils/generators.js";
import { updateDynamicContent } from "./utils/updaters.js";
const BASE_URL = "https://coryd.dev";
const NOT_FOUND_URL = `${BASE_URL}/404`;
export default {
async fetch(request, env) {
const url = new URL(request.url);
const path = url.pathname.replace(/\/$/, "");
const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
let data, type;
if (path === "/books" || path === "/books/")
return fetch(`${BASE_URL}/books/`);
if (path.startsWith("/books/years/")) return fetch(`${BASE_URL}${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(NOT_FOUND_URL, 302);
}
if (!data) return Response.redirect(NOT_FOUND_URL, 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(`${BASE_URL}/dynamic`);
const template = await templateResponse.text();
const metadata = generateMetadata(data, type, globals);
const html = minifyHTML(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 });
},
};

View file

@ -1,14 +0,0 @@
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
const getCountryName = (countryCode) =>
regionNames.of(countryCode.trim()) || countryCode.trim();
export 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(", ");
};

View file

@ -1,26 +0,0 @@
export const fetchDataByUrl = async (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;
};
export const fetchGlobals = async (supabase) => {
const { data, error } = await supabase
.from("optimized_globals")
.select("*")
.single();
if (error) {
console.error("Error fetching globals:", error);
return {};
}
return data;
};

View file

@ -1,26 +0,0 @@
import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
import markdownItFootnote from "markdown-it-footnote";
export const formatDate = (date) =>
new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
export const md = markdownIt({ html: true, linkify: true });
md.use(markdownItAnchor, {
level: [1, 2],
permalink: markdownItAnchor.permalink.headerLink({
safariReaderFix: true,
}),
});
md.use(markdownItFootnote);
export const minifyHTML = (html) =>
html
.replace(/\n\s+/g, "")
.replace(/>\s+</g, "><")
.replace(/<!--.*?-->/g, "");

View file

@ -1,521 +0,0 @@
import truncateHtml from "truncate-html";
import { convert } from "html-to-text";
import { parseCountryField } from "./countries.js";
import { formatDate, md } from "./formatters.js";
import { ICON_MAP } from "./icons.js";
const warningBanner = `<div class="banner warning"><p>${ICON_MAP["alertTriangle"]}There are probably spoilers after this banner — this is a warning about them.</p></div>`;
export const 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["backdrop"] ? data["backdrop"] : 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,
};
};
const generateAssociatedMediaHTML = (data, isGenre = false) => {
const sections = [
{
key: "artists",
icon: "headphones",
category: "music",
title: "Related Artist(s)",
},
{
key: "related_artists",
icon: "headphones",
category: "music",
title: "Related Artist(s)",
},
{
key: "genres",
icon: "headphones",
category: "music",
title: "Related Genre(s)",
},
{
key: "movies",
icon: "film",
category: "movies",
title: "Related Movie(s)",
},
{
key: "related_movies",
icon: "film",
category: "movies",
title: "Related Movie(s)",
},
{
key: "shows",
icon: "deviceTvOld",
category: "tv",
title: "Related Show(s)",
},
{
key: "related_shows",
icon: "deviceTvOld",
category: "tv",
title: "Related Show(s)",
},
{
key: "posts",
icon: "article",
category: "article",
title: "Related Post(s)",
},
{
key: "books",
icon: "books",
category: "books",
title: "Related Book(s)",
},
{
key: "related_books",
icon: "books",
category: "books",
title: "Related Book(s)",
},
];
return sections
.filter(({ key }) => !(isGenre && key === "artists"))
.map(({ key, category, icon, title }) => {
const items = data[key];
if (!items || items.length === 0) return "";
return `
<div class="associated-media">
<span class="${category}">${ICON_MAP[icon]} ${title}</span>
<ul>
${items
.map((item) => {
const name = item.name || item.title;
const url = item.url;
const year = item.year ? ` (${item.year})` : "";
const author = item.author ? ` by ${item.author}` : "";
const totalPlays = item.total_plays
? `&nbsp;<strong class="highlight-text">${item.total_plays} ${
item.total_plays === 1 ? "play" : "plays"
}</strong>`
: "";
let listItemContent = name;
if (key === "artists" || key === "related_artists") {
return `<li><a href="${url}">${name}</a>${totalPlays}</li>`;
} else if (
key === "movies" ||
key === "related_movies" ||
key === "shows" ||
key === "related_shows"
) {
listItemContent = `${name}${year}`;
} else if (key === "books" || key === "related_books") {
listItemContent = `${name}${author}`;
}
return `<li><a href="${url}">${listItemContent}</a></li>`;
})
.join("")}
</ul>
</div>`;
})
.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 `<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}`;
};
export const generateArtistHTML = (artist, globals) => {
const playLabel = artist?.["total_plays"] === 1 ? "play" : "plays";
const concertsList = artist["concerts"]?.length
? `<hr />
<span id="concerts" class="concerts">
${ICON_MAP["deviceSpeaker"]}
I've seen this artist live!
</span>
<ul>${artist["concerts"].map(generateConcertModal).join("")}</ul>`
: "";
const albumsTable = artist["albums"]?.length
? `<table>
<tr><th>Album</th><th>Plays</th><th>Year</th></tr>
${artist["albums"]
.map(
(album) => `
<tr>
<td>${album["name"]}</td>
<td>${album["total_plays"] || 0}</td>
<td>${album["release_year"]}</td>
</tr>`
)
.join("")}
</table>
<p><em>These are the albums by this artist that are in my collection, not necessarily a comprehensive discography.</em></p>
`
: "";
return `
<a href="/music" class="back-link">${ICON_MAP.arrowLeft} Back to music</a>
<article class="artist-focus">
<div class="artist-display">
<img
srcset="
${globals["cdn_url"]}${artist["image"]}?class=w200&type=webp 200w,
${globals["cdn_url"]}${artist["image"]}?class=w600&type=webp 400w,
${globals["cdn_url"]}${artist["image"]}?class=w800&type=webp 800w
"
sizes="(max-width: 450px) 200px,
(max-width: 850px) 400px,
800px"
src="${globals["cdn_url"]}${artist["image"]}?class=w200&type=webp"
alt="${artist["name"]} / ${artist["country"]}"
loading="eager"
decoding="async"
width="200"
height="200"
/>
<div class="media-meta">
<span class="title"><strong>${artist["name"]}</strong></span>
<span class="sub-meta country">${ICON_MAP["mapPin"]} ${parseCountryField(
artist["country"]
)}</span>
${
artist["favorite"]
? `<span class="sub-meta favorite">${ICON_MAP["heart"]} This is one of my favorite artists!</span>`
: ""
}
${
artist["tattoo"]
? `<span class="sub-meta tattoo">${ICON_MAP["needle"]} I have a tattoo inspired by this artist!</span>`
: ""
}
${
artist["total_plays"]
? `<span class="sub-meta"><strong class="highlight-text">${artist["total_plays"]} ${playLabel}</strong></span>`
: ""
}
<span class="sub-meta">${
artist["genre"]
? `<a href="${artist["genre"]["url"]}">${artist["genre"]["name"]}</a>`
: ""
}</span>
</div>
</div>
${generateAssociatedMediaHTML(artist)}
${
artist["description"]
? `
<h2>Overview</h2>
<div data-toggle-content class="text-toggle-hidden">${md.render(
artist["description"]
)}</div>
<button data-toggle-button>Show more</button>`
: ""
}
${concertsList}
${albumsTable}
</article>
`;
};
export const generateBookHTML = (book, globals) => {
const alt = `${book["title"]}${
book["author"] ? ` by ${book["author"]}` : ""
}`;
const percentage = book["progress"] ? `${book["progress"]}%` : "";
const status =
book["status"] === "finished"
? `<span class="sub-meta">Finished on <strong class="highlight-text">${formatDate(
book["date_finished"]
)}</strong></span>`
: percentage
? `<div class="progress-bar-wrapper" title="${percentage}">
<div class="progress-bar" style="width:${percentage}"></div>
</div>`
: "";
return `
<a href="/books" class="back-link">${
ICON_MAP["arrowLeft"]
} Back to books</a>
<article class="book-focus">
<div class="book-display">
<img
srcset="
${globals["cdn_url"]}${
book["image"]
}?class=verticalsm&type=webp 200w,
${globals["cdn_url"]}${
book["image"]
}?class=verticalmd&type=webp 400w,
${globals["cdn_url"]}${
book["image"]
}?class=verticalbase&type=webp 800w
"
sizes="(max-width: 450px) 203px, (max-width: 850px) 406px, 812px"
src="${globals["cdn_url"]}${book["image"]}?class=verticalsm&type=webp"
alt="${alt}"
loading="lazy"
decoding="async"
width="200"
height="307"
/>
<div class="media-meta">
<span class="title"><strong>${book["title"]}</strong></span>
${book["rating"] ? `<span>${book["rating"]}</span>` : ""}
${
book["author"] ? `<span class="sub-meta">By ${book["author"]}</span>` : ""
}
${
book["favorite"]
? `<span class="sub-meta favorite">${ICON_MAP["heart"]} This is one of my favorite books!</span>`
: ""
}
${
book["tattoo"]
? `<span class="sub-meta tattoo">${ICON_MAP["needle"]} I have a tattoo inspired by this book!</span>`
: ""
}
${status ? status : ""}
</div>
</div>
${
book["review"]
? `${warningBanner}<h2>My thoughts</h2><p>${md.render(book["review"])}</p>`
: ""
}
${generateAssociatedMediaHTML(book)}
<h2>Overview</h2>
<p>${md.render(book["description"])}</p>
</article>
`;
};
export const generateGenreHTML = (genre) => {
const artistCount = genre["artists"]?.length || 0;
const connectingWords = artistCount > 1 ? "artists are" : "artist is";
const mediaLinks = generateMediaLinks(genre["artists"], "artist", 5);
return `
<a href="/music" class="back-link">${
ICON_MAP["arrowLeft"]
} Back to music</a>
<h2>${genre["name"]}</h2>
<article class="genre-focus">
${
mediaLinks
? `
<p>My top <strong class="highlight-text">${genre["name"]}</strong> ${connectingWords} ${mediaLinks}. I've listened to <strong class="highlight-text">${genre["total_plays"]}</strong> tracks from this genre.</p>
<hr />`
: ""
}
${generateAssociatedMediaHTML(genre, true)}
${
genre["description"]
? `
<h3>Overview</h3>
<div data-toggle-content class="text-toggle-hidden">
${md.render(genre["description"])}
<p><a href="${
genre["wiki_link"]
}">Continue reading at Wikipedia.</a></p>
<p><em>Wikipedia content provided under the terms of the <a href="https://creativecommons.org/licenses/by-sa/3.0/">Creative Commons BY-SA license</a>.</em></p>
</div>
<button data-toggle-button>Show more</button>`
: ""
}
</article>
`;
};
export const generateWatchingHTML = (media, globals, type) => {
const isShow = type === "show";
const label = isShow ? "show" : "movie";
const lastWatched =
media["last_watched"] || (isShow && media["episode"]?.["last_watched_at"]);
return `
<a href="/watching" class="back-link">${
ICON_MAP.arrowLeft
} Back to watching</a>
<article class="watching focus">
<img
srcset="
${globals["cdn_url"]}${
media["backdrop"]
}?class=bannersm&type=webp 256w,
${globals["cdn_url"]}${
media["backdrop"]
}?class=bannermd&type=webp 512w,
${globals["cdn_url"]}${
media["backdrop"]
}?class=bannerbase&type=webp 1024w
"
sizes="(max-width: 450px) 256px,
(max-width: 850px) 512px,
1024px"
src="${globals["cdn_url"]}${media["backdrop"]}?class=bannersm&type=webp"
alt="${media["title"]} / ${media["year"]}"
class="image-banner"
loading="eager"
decoding="async"
width="256"
height="180"
/>
<div class="media-meta">
<span class="title"><strong>${media["title"]}</strong> (${
media["year"]
})</span>
${media["rating"] ? `<span>${media["rating"]}</span>` : ""}
${
media["favorite"]
? `<span class="sub-meta favorite">${ICON_MAP["heart"]} This is one of my favorite ${label}s!</span>`
: ""
}
${
media["tattoo"]
? `<span class="sub-meta tattoo">${ICON_MAP["needle"]} I have a tattoo inspired by this ${label}!</span>`
: ""
}
${
media["collected"]
? `<span class="sub-meta collected">${ICON_MAP["circleCheck"]} This ${label} is in my collection!</span>`
: ""
}
${
lastWatched
? `<span class="sub-meta">Last watched on <strong class="highlight-text">${formatDate(
lastWatched
)}</strong>.</span>`
: ""
}
</div>
${
media["review"]
? `${warningBanner}<h2>My thoughts</h2><p>${md.render(
media["review"]
)}</p>`
: ""
}
${generateAssociatedMediaHTML(media)}
${
media["description"]
? `<h2>Overview</h2><p>${md.render(media["description"])}</p>`
: ""
}
</article>
`;
};
export const generateConcertModal = (concert) => {
const venue = concert["venue_name"]
? concert["venue_latitude"] && concert["venue_longitude"]
? `<a href="https://www.openstreetmap.org/?mlat=${concert["venue_latitude"]}&mlon=${concert["venue_longitude"]}#map=18/${concert["venue_latitude"]}/${concert["venue_longitude"]}">${concert["venue_name_short"]}</a>`
: concert["venue_name_short"]
: "";
const notesModal = concert["notes"]
? `<input class="modal-input" id="${
concert["id"]
}" type="checkbox" tabindex="0" />
<label class="modal-toggle" for="${concert["id"]}">${
ICON_MAP["infoCircle"]
}</label>
<div class="modal-wrapper">
<div class="modal-body">
<label class="modal-close" for="${concert["id"]}">${
ICON_MAP["circleX"]
}</label>
<div>
<h3>Notes</h3>
${md.render(concert["notes"])}
</div>
</div>
</div>`
: "";
return `
<li>
<strong class="highlight-text">${formatDate(
concert["date"]
)}</strong> at ${venue}
${notesModal}
</li>
`;
};

View file

@ -1,18 +0,0 @@
export const ICON_MAP = {
alertTriangle: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-alert-triangle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 9v4" /><path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z" /><path d="M12 16h.01" /></svg>`,
arrowLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0"/><path d="M5 12l6 6"/><path d="M5 12l6 -6"/></svg>`,
article: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-article"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 8h10" /><path d="M7 12h10" /><path d="M7 16h10" /></svg>`,
books: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-books"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M9 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M5 8h4" /><path d="M9 16h4" /><path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041z" /><path d="M14 9l4 -1" /><path d="M16 16l3.923 -.98" /></svg>`,
circleCheck: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/><path d="M9 12l2 2l4 -4"/></svg>`,
circleX: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-x"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M10 10l4 4m0 -4l-4 4" /></svg>`,
deviceSpeaker: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-speaker"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 3m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z" /><path d="M12 14m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M12 7l0 .01" /></svg>`,
deviceTvOld: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-tv-old"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M16 3l-4 4l-4 -4" /><path d="M15 7v13" /><path d="M18 15v.01" /><path d="M18 12v.01" /></svg>`,
film: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-movie"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M8 4l0 16" /><path d="M16 4l0 16" /><path d="M4 8l4 0" /><path d="M4 16l4 0" /><path d="M4 12l16 0" /><path d="M16 8l4 0" /><path d="M16 16l4 0" /></svg>`,
headphones: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-headphones"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M15 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M4 15v-3a8 8 0 0 1 16 0v3" /></svg>`,
heart: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-heart"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572"/></svg>`,
infoCircle: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-info-circle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M12 9h.01" /><path d="M11 12h1v4h1" /></svg>`,
link: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-link"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 15l6 -6" /><path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /><path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /></svg>`,
mapPin: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-map-pin"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /><path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z" /></svg>`,
needle: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-needle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 21c-.667 -.667 3.262 -6.236 11.785 -16.709a3.5 3.5 0 1 1 5.078 4.791c-10.575 8.612 -16.196 12.585 -16.863 11.918z"/><path d="M17.5 6.5l-1 1"/></svg>`,
movie: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-movie"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6h16M4 12h16M4 18h16M10 4v2M14 4v2M10 12v2M14 12v2M10 18v2M14 18v2"/></svg>`,
};

View file

@ -1,49 +0,0 @@
import { parseHTML } from "linkedom";
export const 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();
};

View file

@ -1,19 +0,0 @@
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}" },
]

View file

@ -5,7 +5,7 @@ import { createClient } from "@supabase/supabase-js";
const BASE_URL = "https://coryd.dev";
export default {
async scheduled(event, env, ctx) {
async scheduled(event, env) {
await handleMastodonPost(env);
},
};
@ -14,8 +14,8 @@ async function handleMastodonPost(env) {
const mastodonApiUrl = "https://follow.coryd.dev/api/v1/statuses";
const accessToken = env.MASTODON_ACCESS_TOKEN;
const rssFeedUrl = "https://coryd.dev/feeds/syndication";
const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY;
const supabaseUrl = env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
try {
@ -56,7 +56,6 @@ async function handleMastodonPost(env) {
accessToken,
content
);
const timestamp = new Date().toISOString();
await env.RSS_TO_MASTODON_NAMESPACE.put(link, timestamp);
@ -101,6 +100,8 @@ function truncateContent(title, description, link, maxLength) {
.slice(0, -1)
.join(" ") + "...";
truncatedDescription = truncatedDescription.replace(/\s+([.,!?;:])/g, "$1");
return `${title}\n\n${truncatedDescription}\n\n${link}`;
}

View file

@ -2,8 +2,8 @@ import { createClient } from "@supabase/supabase-js";
export default {
async fetch(request, env) {
const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY;
const supabaseUrl = env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
const { data, error } = await supabase

View file

@ -67,8 +67,8 @@ const sendEmail = async (subject, text, authHeader, maxRetries = 3) => {
export default {
async fetch(request, env) {
const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY;
const supabaseUrl = env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY;
const FORWARDEMAIL_API_KEY = env.FORWARDEMAIL_API_KEY;
const ACCOUNT_ID_PLEX = env.ACCOUNT_ID_PLEX;
const supabase = createClient(supabaseUrl, supabaseKey);

View file

@ -2,8 +2,8 @@ import { createClient } from "@supabase/supabase-js";
export default {
async fetch(request, env) {
const supabaseUrl = env.SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY || process.env.SUPABASE_KEY;
const supabaseUrl = env.SUPABASE_URL;
const supabaseKey = env.SUPABASE_KEY;
const supabase = createClient(supabaseUrl, supabaseKey);
try {