feat: numerous other pages

This commit is contained in:
Cory Dransfeldt 2024-11-16 22:03:37 -08:00
parent 159b60b3fb
commit ca34a11ad4
No known key found for this signature in database
54 changed files with 1074 additions and 101 deletions

1
.env
View file

@ -1,4 +1,5 @@
ACCOUNT_ID_PLEX=
API_KEY_PLAUSIBLE=
SUPABASE_URL=
SUPABASE_KEY=
CF_ACCOUNT_ID=

View file

@ -5,11 +5,11 @@
# feeds
/feed.xml /feeds/posts 301
/follow.xml /feeds/all 301
/feeds/posts.xml /feeds/posts 301
/feeds/links.xml /feeds/links 301
/feeds/books.xml /feeds/books 301
/feeds/movies.xml /feeds/movies 301
/feeds/all.xml /feeds/all 301
/feeds/posts /feeds/posts.xml 301
/feeds/links /feeds/links.xml 301
/feeds/books /feeds/books.xml 301
/feeds/movies /feeds/movies.xml 301
/feeds/all /feeds/all.xml 301
/feeds/posts/ /feeds/posts 301
/feeds/links/ /feeds/links 301
/feeds/books/ /feeds/books 301

View file

@ -15,10 +15,10 @@ export default defineConfig({
},
resolve: {
alias: {
"@cdransf": "node_modules/@cdransf",
"@components": "/src/components",
"@data": "/src/utils/data",
"@layouts": "/src/layouts",
"@npm": "/node_modules",
"@scripts": "/src/scripts",
"@styles": "/src/styles",
"@utils": "/src/utils",

76
package-lock.json generated
View file

@ -12,10 +12,12 @@
"@astrojs/react": "^3.6.2",
"@tabler/icons-react": "^3.19.0",
"astro": "^4.16.13",
"luxon": "^3.5.0"
"luxon": "^3.5.0",
"youtube-video-element": "^1.1.6"
},
"devDependencies": {
"@supabase/supabase-js": "^2.45.4",
"ics": "^3.8.1",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-footnote": "^4.0.0",
@ -3405,6 +3407,18 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"license": "BSD-2-Clause"
},
"node_modules/ics": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz",
"integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==",
"dev": true,
"license": "ISC",
"dependencies": {
"nanoid": "^3.1.23",
"runes2": "^1.1.2",
"yup": "^1.2.0"
}
},
"node_modules/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@ -5108,6 +5122,13 @@
"node": ">=6"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"dev": true,
"license": "MIT"
},
"node_modules/property-information": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
@ -5598,6 +5619,13 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/runes2": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz",
"integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@ -5877,6 +5905,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"dev": true,
"license": "MIT"
},
"node_modules/tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
@ -5911,6 +5946,13 @@
"node": ">=8.0"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"dev": true,
"license": "MIT"
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -6943,6 +6985,38 @@
"stacktracey": "^2.1.8"
}
},
"node_modules/youtube-video-element": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.1.6.tgz",
"integrity": "sha512-EaHyEh68twtuWn6S7cCEghJkLfaOD82wmJhczeWSTxT71yOG6lL7EXu6EAHADj6wPQJ9+lZpaos3f/Bh8Lzvjg==",
"license": "MIT"
},
"node_modules/yup": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz",
"integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/yup/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",

View file

@ -16,10 +16,12 @@
"@astrojs/react": "^3.6.2",
"@tabler/icons-react": "^3.19.0",
"astro": "^4.16.13",
"luxon": "^3.5.0"
"luxon": "^3.5.0",
"youtube-video-element": "^1.1.6"
},
"devDependencies": {
"@supabase/supabase-js": "^2.45.4",
"ics": "^3.8.1",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-footnote": "^4.0.0",

View file

@ -1,35 +0,0 @@
---
import AssociatedMedia from '@components/blocks//AssociatedMedia.astro';
import GitHub from '@components/blocks/banners/GitHub.astro';
import Hero from '@components/blocks//Hero.astro';
import Modal from '@components/blocks//Modal.astro';
import YouTubePlayer from '@components/blocks//YouTubePlayer.astro';
import { md } from '@utils/helpers.js';
const { block } = Astro.props;
const htmlContent = block.type === 'markdown' ? md(block.text) : '';
---
{block.type === 'youtube_player' && (
<YouTubePlayer url={block.url} />
)}
{block.type === 'hero' && (
<Hero image={block.image} alt={block.alt} />
)}
{block.type === 'markdown' && (
<div set:html={htmlContent}></div>
)}
{block.type === 'modal' && (
<Modal content={block.content} />
)}
{block.type === 'associated_media' && (
<AssociatedMedia media={block.media} />
)}
{block.type === 'github_banner' && (
<GitHub url={block.url} />
)}

View file

@ -1,5 +1,5 @@
---
import NavLink from './nav/NavLink.astro';
import NavLink from '@components/nav/NavLink.astro';
const { nav, updated } = Astro.props;
---

View file

@ -1,5 +1,5 @@
---
import Menu from './nav/Menu.astro';
import Menu from '@components/nav/Menu.astro';
const { nav, siteName, url } = Astro.props;
const isHomePage = url === '/';

View file

@ -0,0 +1,155 @@
---
const {
schema,
page,
globals,
post,
title,
description,
artist,
movie,
movies,
show,
tv,
book,
books,
genre,
year,
} = Astro.props;
const fullUrl = `${globals.url}${page.url}`;
let pageTitle = globals.site_name;
let pageDescription = globals.site_description;
let ogImage = `${globals.cdn_url}${globals.avatar}`;
switch (schema) {
case "blog":
pageTitle = post?.title || pageTitle;
pageDescription = post?.description || pageDescription;
ogImage = `${globals.cdn_url}${post?.image}`;
break;
case "music":
case "music-index":
case "music-period":
pageTitle = schema === "music" ? `Music / ${page.title}` : "Music";
if (schema === "music") {
ogImage = `${globals.cdn_url}${page?.image}`;
} else if (schema === "music-index") {
ogImage = `${globals.cdn_url}${movies?.week?.artists[0]?.grid?.image}`;
} else if (schema === "music-period") {
ogImage = `${globals.cdn_url}${page?.image}`;
}
break;
case "artist":
pageTitle = `Artists / ${artist?.name}`;
pageDescription = artist?.description || pageDescription;
ogImage = `${globals.cdn_url}${artist?.image}`;
break;
case "genre":
pageTitle = `Music / ${genre?.name}`;
pageDescription = genre?.description || pageDescription;
ogImage = `${globals.cdn_url}${genre?.artists[0]?.image}`;
break;
case "book":
pageTitle = `Books / ${book?.title} by ${book?.author}`;
pageDescription = book?.review || book?.description || pageDescription;
ogImage = `${globals.cdn_url}${book?.image}`;
break;
case "books":
pageTitle = "Books";
const overviewBook = books?.all?.find((b) => b.status === "started");
ogImage = `${globals.cdn_url}${overviewBook?.image}`;
break;
case "books-year":
const bookYear = year?.data?.find((b) => b.status === "finished");
ogImage = `${globals.cdn_url}${bookYear?.image}`;
break;
case "movie":
pageTitle = `Movies / ${movie?.title}`;
if (movie?.rating) {
pageTitle += ` (${movie.rating})`;
}
pageDescription = movie?.review || movie?.description || pageDescription;
ogImage = `${globals.cdn_url}${movie?.backdrop}`;
break;
case "favorite-movies":
pageTitle = "Favorite movies";
const favoriteMovie = movies?.favorites?.[0];
ogImage = `${globals.cdn_url}${favoriteMovie?.backdrop}`;
break;
case "show":
pageTitle = `Show / ${show?.title}`;
pageDescription = show?.review || show?.description || pageDescription;
ogImage = `${globals.cdn_url}${show?.backdrop}`;
break;
case "favorite-shows":
pageTitle = "Favorite shows";
const favoriteShow = tv?.favorites?.[0];
ogImage = `${globals.cdn_url}${favoriteShow?.backdrop}`;
break;
case "watching":
pageTitle = "Watching";
const overviewMovie = movies?.recentlyWatched?.[0];
ogImage = `${globals.cdn_url}${overviewMovie?.backdrop}`;
break;
case "page":
pageTitle = page?.title || pageTitle;
pageDescription = page?.description || pageDescription;
break;
default:
break;
}
if (title) pageTitle = title;
if (description) pageDescription = description;
if (pageTitle !== globals.site_name && schema !== "blog") pageTitle = `${pageTitle} / ${globals.site_name}`;
const escapedPageDescription =
pageDescription?.replace(
/["'<>&]/g,
(c) =>
({
'"': "&quot;",
"'": "&#39;",
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
})[c]
) || "";
---
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>{pageTitle}</title>
<link rel="canonical" href={fullUrl} />
<meta property="og:title" content={pageTitle} />
<meta name="description" content={escapedPageDescription} />
<meta property="og:description" content={escapedPageDescription} />
<meta property="og:type" content="article" />
<meta property="og:url" content={fullUrl} />
<meta property="og:image" content={`${ogImage}?class=w800`} />
<meta name="theme-color" content={globals.theme_color} />
<meta name="fediverse:creator" content={globals.mastodon} />
<meta name="generator" content="Astro" />
<meta name="robots" content="noai, noimageai" />
<link
href={`${globals.cdn_url}${globals.avatar_transparent}?class=w50`}
rel="icon"
sizes="any"
/>
<link
href={`${globals.cdn_url}${globals.avatar_transparent}?class=w50&type=svg`}
rel="icon"
type="image/svg+xml"
/>
<link
href={`${globals.cdn_url}${globals.avatar}?class=w800`}
rel="apple-touch-icon"
/>
<link
type="application/atom+xml"
rel="alternate"
title={`Posts / ${globals.site_name}`}
href="https://coryd.dev/feeds/posts"
/>

View file

@ -0,0 +1,64 @@
---
import { fetchAllPosts } from "@data/posts.js";
import { fetchAnalyticsData } from "@data/analytics.js";
import { fetchLinks } from "@data/links.js";
import AddonLinks from '@components/blocks/links/AddonLinks.astro';
import AssociatedMedia from '@components/blocks//AssociatedMedia.astro';
import GitHub from '@components/blocks/banners/GitHub.astro';
import Hero from '@components/blocks//Hero.astro';
import Modal from '@components/blocks//Modal.astro';
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 { 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;
---
{block.type === 'addon_links' && (
<AddonLinks popularPosts={popularPosts} links={links} />
)}
{block.type === 'associated_media' && (
<AssociatedMedia media={block.media} />
)}
{block.type === 'divider' && (
<div set:html={md(block.markup)}></div>
)}
{block.type === 'github_banner' && (
<GitHub url={block.url} />
)}
{block.type === 'hero' && (
<Hero image={block.image} alt={block.alt} />
)}
{block.type === 'markdown' && (
<div set:html={md(block.text)}></div>
)}
{block.type === 'npm_banner' && (
<Npm url={block.url} command={block.command} />
)}
{block.type === 'modal' && (
<Modal content={block.content} />
)}
{block.type === 'rss_banner' && (
<Rss url={block.url} text={block.text} />
)}
{block.type === 'youtube_player' && (
<YouTubePlayer url={block.url} />
)}

View file

@ -1,7 +0,0 @@
---
const { post } = Astro.props;
---
<article class="mastodon-post">
<p>{post.content}</p>
</article>

View file

@ -1,5 +1,5 @@
---
import { fetchNowPlaying } from '../../utils/data/nowPlaying.js';
import { fetchNowPlaying } from '@utils/data/nowPlaying.js';
const isProduction = import.meta.env.MODE === 'production';
const nowPlayingData = await fetchNowPlaying();

View file

@ -1,10 +1,7 @@
---
import YoutubeVideo from '@npm/youtube-video-element/dist/react.js';
const { url } = Astro.props;
---
<iframe
width="560"
height="315"
src={url}
allowfullscreen>
</iframe>
<YoutubeVideo src={url} controls />

View file

@ -0,0 +1,15 @@
---
import { IconBrandMastodon } from "@tabler/icons-react";
const { url } = Astro.props;
---
<div class="banner mastodon">
<p>
<a
class="mastodon plausible-event-name=Discuss+on+Mastodon+post+footer"
href={url}
>
<IconBrandMastodon size={24} /> Discuss this post on Mastodon.
</a>
</p>
</div>

View file

@ -0,0 +1,10 @@
---
import PopularPosts from './PopularPosts.astro';
import RecentLinks from './RecentLinks.astro';
const { popularPosts, links } = Astro.props;
---
<div class="addon-links">
<PopularPosts popularPosts={popularPosts} />
<RecentLinks links={links} />
</div>

View file

@ -0,0 +1,21 @@
---
const { popularPosts } = Astro.props;
import { IconChartBarPopular } from '@tabler/icons-react';
---
{popularPosts && popularPosts.length > 0 && (
<article>
<h3>
<a class="article" href="/posts">
<IconChartBarPopular size={24} />
Popular posts
</a>
</h3>
<ol type="1">
{popularPosts.slice(0, 5).map((post) => (
<li>
<a href={post.url}>{post.title}</a>
</li>
))}
</ol>
</article>
)}

View file

@ -0,0 +1,26 @@
---
const { links } = Astro.props;
import { IconLink } from '@tabler/icons-react';
---
{links && links.length > 0 && (
<article>
<h3>
<a class="link" href="/links">
<IconLink size={24} />
Recent links
</a>
</h3>
<ul>
{links.slice(0, 5).map((link) => (
<li>
<a href={link.link} title={link.title}>
{link.title}
</a>
{link.author && (
<> via <a href={link.author.url}>{link.author.name}</a></>
)}
</li>
))}
</ul>
</article>
)}

View file

@ -1,6 +1,6 @@
---
import { IconClock, IconStar, IconArrowRight } from '@tabler/icons-react';
import { fetchAllPosts } from '../utils/data/posts.js';
import { fetchAllPosts } from '@utils/data/posts.js';
import { md } from '@utils/helpers.js';
const posts = await fetchAllPosts();

View file

@ -0,0 +1,70 @@
---
import Paginator from '@components/nav/Paginator.astro';
const { data, globals, count, shape, pagination, loading = "lazy" } = Astro.props;
const pageCount = pagination?.pages?.length || 0;
const hidePagination = pageCount <= 1;
function getImageAttributes(item, shape) {
let imageUrl = item.grid.image;
let imageClass = '';
let width = 0;
let height = 0;
switch (shape) {
case 'poster':
imageUrl = item.grid.backdrop;
imageClass = 'banner';
width = 256;
height = 170;
break;
case 'square':
imageClass = 'square';
width = 200;
height = 200;
break;
case 'vertical':
imageClass = 'vertical';
width = 200;
height = 307;
break;
}
return { imageUrl, imageClass, width, height };
}
---
<div class={`media-grid ${shape}`}>
{data.slice(0, count).map((item) => {
const alt = item.grid.alt?.replace(/['"]/g, '');
const { imageUrl, imageClass, width, height } = getImageAttributes(item, shape);
return (
<a href={item.grid.url} title={alt}>
<div class="item media-overlay">
<div class="meta-text">
<div class="header">{item.grid.title}</div>
<div class="subheader">{item.grid.subtext}</div>
</div>
<img
srcSet={`
${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp ${width}w,
${globals.cdn_url}${imageUrl}?class=${imageClass}md&type=webp ${width * 2}w
`}
sizes={`(max-width: 450px) ${width}px, ${width * 2}px`}
src={`${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp`}
alt={alt}
loading={loading}
decoding="async"
width={width}
height={height}
/>
</div>
</a>
);
})}
</div>
{!hidePagination && (
<Paginator pagination={pagination} />
)}

View file

@ -0,0 +1,9 @@
---
const { percentage } = Astro.props;
---
{percentage && (
<div class="progress-bar-wrapper" title={percentage}>
<div style={`width: ${percentage}`} class="progress-bar"></div>
</div>
)}

View file

@ -0,0 +1,29 @@
---
import ProgressBar from '@components/media/ProgressBar.astro';
const { data, count } = Astro.props;
---
<div class="music-chart">
<ol type="1">
{data.slice(0, count).map((item) => {
const percentage = `${item.chart.percentage}%`;
const playsLabel = item.chart.plays === 1 ? 'play' : 'plays';
return (
<li value={item.chart.rank}>
<div class="item">
<div class="info">
<a class="title" href={item.chart.url}>{item.chart.title}</a>
<span class="subtext">{item.chart.artist}</span>
<span class="subtext">
{item.chart.plays} {playsLabel}
</span>
</div>
<ProgressBar percentage={percentage} />
</div>
</li>
);
})}
</ol>
</div>

View file

@ -0,0 +1,41 @@
---
const { data, globals } = Astro.props;
---
<div class="music-chart">
{data.slice(0, 10).map((item) => (
<div class="item">
<div class="meta">
<a href={item.chart.url}>
<img
srcSet={`
${globals.cdn_url}${item.chart.image}?class=w50&type=webp 50w,
${globals.cdn_url}${item.chart.image}?class=w100&type=webp 100w
`}
sizes="(max-width: 450px) 50px, 100px"
src={`${globals.cdn_url}${item.chart.image}?class=w50&type=webp`}
alt={item.chart.alt.replace(/['"]/g, '')}
loading="lazy"
decoding="async"
width="64"
height="64"
/>
</a>
<div class="meta-text">
<a class="title" href={item.chart.url}>{item.chart.title}</a>
<span class="subtext">{item.chart.subtext}</span>
</div>
</div>
<time dateTime={item.chart.played_at}>
{new Date(item.chart.played_at).toLocaleString("en-US", {
timeZone: "America/Los_Angeles",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
})}
</time>
</div>
))}
</div>

View file

@ -0,0 +1,18 @@
---
import Hero from "@components/blocks/Hero.astro";
const { movie, globals } = Astro.props;
---
<a href={movie.url}>
<div class="watching media-overlay hero">
<div class="meta-text">
<div class="header">{movie.title}</div>
<div class="subheader">
{movie.rating && <span class="rating">{movie.rating} </span>}
({movie.year})
</div>
</div>
<Hero globals={globals} image={movie.backdrop} alt={movie.title} />
</div>
</a>

1
src/env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

View file

@ -1,8 +1,8 @@
---
import "@styles/index.css";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { fetchNavigation } from "../utils/data/nav.js";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import { fetchNavigation } from "@utils/data/nav.js";
const currentUrl = Astro.url.pathname;
const nav = await fetchNavigation();

View file

@ -0,0 +1,46 @@
import { fetchGlobals } from "@utils/data/globals";
export async function GET() {
try {
const globals = await fetchGlobals();
const webfingerResponse = {
subject: `acct:${globals.webfinger_username}@${globals.webfinger_hostname}`,
aliases: [
`https://${globals.webfinger_hostname}/@${globals.webfinger_username}`,
`https://${globals.webfinger_hostname}/users/${globals.webfinger_username}`,
],
links: [
{
rel: "http://webfinger.net/rel/profile-page",
type: "text/html",
href: `https://${globals.webfinger_hostname}/@${globals.webfinger_username}`,
},
{
rel: "self",
type: "application/activity+json",
href: `https://${globals.webfinger_hostname}/users/${globals.webfinger_username}`,
},
{
rel: "http://ostatus.org/schema/1.0/subscribe",
template: `https://${globals.webfinger_hostname}/authorize_interaction?uri={uri}`,
},
{
rel: "http://webfinger.net/rel/avatar",
type: "image/png",
href: `${globals.cdn_url}${globals.avatar}?class=squarebase`,
},
],
};
return new Response(JSON.stringify(webfingerResponse), {
status: 200,
headers: {
"Content-Type": "application/jrd+json",
},
});
} catch (error) {
console.error("Error generating WebFinger response:", error);
return new Response("Error generating WebFinger response", { status: 500 });
}
}

View file

@ -1,6 +1,6 @@
---
import Layout from '@layouts/Layout.astro';
import BlockRenderer from '@components/BlockRenderer.astro';
import BlockRenderer from '@components/blocks/BlockRenderer.astro';
import { fetchGlobals } from '@utils/data/globals.js';
import { fetchPages } from '@utils/data/pages';

View file

@ -0,0 +1,22 @@
import { generateJsonFeed } from '@utils/generateJsonFeed';
import { fetchGlobals } from '@utils/data/globals';
import { fetchActivity } from '@utils/data/activity';
export async function GET() {
const globals = await fetchGlobals();
const activity = await fetchActivity();
const feed = generateJsonFeed({
permalink: "/feeds/all.json",
title: "All activity / Cory Dransfeldt",
globals,
data: activity,
});
return new Response(feed, {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -0,0 +1,22 @@
import { generateJsonFeed } from '@utils/generateJsonFeed';
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 feed = generateJsonFeed({
permalink: "/feeds/movies.json",
title: "Movies / Cory Dransfeldt",
globals,
data: movies.feed,
});
return new Response(feed, {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -0,0 +1,22 @@
import { generateRssFeed } from "@utils/generateRssFeed";
import { fetchGlobals } from "@utils/data/globals";
import { fetchActivity } from "@utils/data/activity";
export async function GET() {
const globals = await fetchGlobals();
const activity = await fetchActivity();
const rss = generateRssFeed({
permalink: "/feeds/all.xml",
title: "All activity / Cory Dransfeldt",
globals,
data: activity,
});
return new Response(rss, {
status: 200,
headers: {
"Content-Type": "application/rss+xml",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -0,0 +1,22 @@
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",
},
});
}

View file

@ -1,5 +1,5 @@
import fetchSyndication from '../../utils/data/syndication.js';
import { fetchGlobals } from '../../utils/data/globals.js';
import fetchSyndication from '@utils/data/syndication.js';
import { fetchGlobals } from '@utils/data/globals.js';
export async function GET() {
const globals = await fetchGlobals();

29
src/pages/humans.txt.js Normal file
View file

@ -0,0 +1,29 @@
import { fetchGlobals } from '@utils/data/globals';
export async function GET() {
try {
const globals = await fetchGlobals();
const humansTxt = `
## team
${globals.site_name}
${globals.url}
${globals.mastodon}
## colophon
${globals.url}/colophon
`.trim();
return new Response(humansTxt, {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
});
} catch (error) {
console.error('Error generating humans.txt:', error);
return new Response('Error generating humans.txt', { status: 500 });
}
}

View file

@ -1,9 +1,9 @@
---
import Layout from '../layouts/Layout.astro';
import Intro from '../components/Intro.astro';
import { fetchGlobals } from '../utils/data/globals';
import RecentActivity from '../components/RecentActivity.astro';
import RecentPosts from '../components/RecentPosts.astro';
import { fetchGlobals } from '@utils/data/globals';
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';
const globals = await fetchGlobals();
const schema = 'blog';

View file

@ -0,0 +1,24 @@
import { albumReleasesCalendar } from '@utils/albumReleasesCalendar';
import { fetchAlbumReleases } from '@utils/data/albumReleases';
export async function GET() {
try {
const { all: albumReleases } = await fetchAlbumReleases();
const icsContent = await albumReleasesCalendar(albumReleases);
if (!icsContent) return new Response('Error generating ICS file', { status: 500 });
return new Response(icsContent, {
status: 200,
headers: {
'Content-Type': 'text/calendar',
'Content-Disposition': 'attachment; filename="releases.ics"',
},
});
} catch (error) {
console.error('Error generating album releases ICS file:', error);
return new Response('Error generating album releases ICS file', {
status: 500,
});
}
}

View file

@ -55,5 +55,5 @@ const pagination = {
</article>
))}
<Paginator pagination={pagination} appVersion="1.0.0" />
<Paginator pagination={pagination} />
</Layout>

View file

@ -1,14 +1,26 @@
---
import { IconStar } from "@tabler/icons-react";
import { fetchAllPosts } from "@data/posts.js";
import { fetchAnalyticsData } from "@data/analytics.js";
import { fetchGlobals } from "@data/globals.js";
import { fetchLinks } from "@data/links.js";
import { md } from '@utils/helpers.js';
import OldPost from "@components/blocks/banners/OldPost.astro";
import BlockRenderer from "@components/BlockRenderer.astro";
import { getPopularPosts } from '@utils/getPopularPosts.js';
const analytics = await fetchAnalyticsData();
const links = await fetchLinks();
const posts = await fetchAllPosts();
const popularPosts = getPopularPosts(posts, analytics);
import AddonLinks from '@components/blocks/links/AddonLinks.astro';
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
import MastodonPost from "@components/blocks/MastodonPost.astro";
import Layout from "@layouts/Layout.astro";
import BlockRenderer from "@components/blocks/BlockRenderer.astro";
import Coffee from "@components/blocks/banners/Coffee.astro";
import Layout from "@layouts/Layout.astro";
import Mastodon from "@components/blocks/banners/Mastodon.astro";
import OldPost from "@components/blocks/banners/OldPost.astro";
export const prerender = true;
@ -87,7 +99,7 @@ const htmlContent = md(post.content);
post.blocks &&
post.blocks.map((block) => <BlockRenderer block={block} />)
}
<!-- {post.mastodon_url && <MastodonPost url={post.mastodon_url} />} -->
{post.mastodon_url && <Mastodon url={post.mastodon_url} />}
<AssociatedMedia
artists={post.artists}
books={post.books}
@ -97,6 +109,7 @@ const htmlContent = md(post.content);
shows={post.shows}
/>
<Coffee />
<AddonLinks popularPosts={popularPosts} links={links} />
</div>
</article>
</Layout>

View file

@ -1,4 +1,4 @@
import { fetchAllRobots } from '../utils//data/robots.js';
import { fetchAllRobots } from '@utils//data/robots.js';
export async function GET() {
try {

View file

@ -24,6 +24,7 @@
&.coffee,
&.error,
&.github,
&.mastodon,
&.npm,
&.old-post,
&.rss,
@ -37,6 +38,9 @@
&.github {
--banner-accent-color: var(--brand-github);
}
&.mastodon {
--banner-accent-color: var(--brand-mastodon);
}
&.npm {
--banner-accent-color: var(--brand-npm);
}

View file

@ -1,22 +0,0 @@
mastodon-post {
width: 100%;
.mastodon-post-wrapper {
& dl,
& dt {
display: flex;
}
& dl {
align-items: center;
& dd {
margin-left: var(--spacing-xs);
&:not(:last-child) {
margin-right: var(--spacing-lg);
}
}
}
}
}

View file

@ -29,7 +29,6 @@
@import url("./components/banners.css") layer(components);
@import url("./components/buttons.css") layer(components);
@import url("./components/forms.css") layer(components);
@import url("./components/mastodon-post.css") layer(components);
@import url("./components/media-grid.css") layer(components);
@import url("./components/menu.css") layer(components);
@import url("./components/modal.css") layer(components);

View file

@ -0,0 +1,38 @@
import { DateTime } from "luxon";
import { createEvents } from "ics";
export async function albumReleasesCalendar(albumReleases) {
if (!albumReleases || albumReleases.length === 0) return "";
const events = albumReleases
.map((album) => {
const date = DateTime.fromISO(album["release_date"]);
if (!date.isValid) return null;
return {
start: [date.year, date.month, date.day],
startInputType: "local",
startOutputType: "local",
title: `Release: ${album["artist"]["name"]} - ${album["title"]}`,
description: `Check out this new album release: ${album["url"]}. Read more about ${album["artist"]["name"]} at https://coryd.dev${album["artist"]["url"]}`,
url: album["url"],
uid: `${date.toFormat("yyyyMMdd")}-${album["artist"]["name"]}-${album["title"]}@coryd.dev`,
timestamp: DateTime.now().toUTC().toFormat("yyyyMMdd'T'HHmmss'Z'"),
};
})
.filter((event) => event !== null);
const { error, value } = createEvents(events, {
calName: "Album releases calendar / coryd.dev",
});
if (error) {
console.error("Error creating events: ", error);
events.forEach((event, index) => {
console.error(`Event ${index}:`, event);
});
return "";
}
return value;
}

View file

@ -0,0 +1,34 @@
let cachedPages = null;
export async function fetchAnalyticsData() {
if (import.meta.env.MODE === "development" && cachedPages) return cachedPages;
const API_KEY_PLAUSIBLE = import.meta.env.API_KEY_PLAUSIBLE;
const url =
"https://plausible.io/api/v1/stats/breakdown?site_id=coryd.dev&period=6mo&property=event:page&limit=30";
try {
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${API_KEY_PLAUSIBLE}`,
},
});
if (!res.ok) {
console.error(`Error fetching Plausible data: ${res.statusText}`);
return [];
}
const pages = await res.json();
const filteredPages = pages["results"].filter((p) =>
p["page"].includes("posts")
);
if (import.meta.env.MODE === "development") cachedPages = filteredPages;
return filteredPages;
} catch (error) {
console.error("Error fetching Plausible data:", error);
return [];
}
}

View file

@ -0,0 +1,39 @@
export function generateJsonFeed({
permalink,
title,
globals,
data,
}) {
const feed = {
version: "https://jsonfeed.org/version/1",
title,
home_page_url: globals.url,
feed_url: `${globals.url}${permalink}`,
description: globals.site_description,
icon: `${globals.cdn_url}${globals.avatar}?class=w200`,
author: {
name: globals.site_name,
url: globals.url,
avatar: `${globals.cdn_url}${globals.avatar}?class=w200`,
},
items: data.slice(0, 20).map((entry) => {
const text = entry.feed.description
?.replace(/(<([^>]+)>)/gi, "")
?.trim()
?.replace(/\s+/g, " ")
?.slice(0, 200);
return {
id: entry.feed.url,
url: entry.feed.url,
title: entry.feed.title,
content_html: text,
content_text: text,
summary: text,
date_published: new Date(entry.feed.date).toISOString(),
};
}),
};
return JSON.stringify(feed, null, 2);
}

View file

@ -0,0 +1,45 @@
import { DateTime } from "luxon";
export function generateRssFeed({ permalink, title, globals, data }) {
const rssItems = data.slice(0, 20).map((entry) => {
const entryFeed = entry.feed;
const rating = entry.rating;
const entryTitle = `${entryFeed.title}${
entryFeed.artist?.name ? ` via ${entryFeed.artist.name}` : ""
}${rating ? ` (${rating})` : ""}`;
return `
<item>
<title><![CDATA[${entryTitle}]]></title>
<link>${entryFeed.url}</link>
<pubDate>${DateTime.fromISO(entryFeed.date).toRFC2822()}</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>
</item>`;
});
return `
<?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>${DateTime.now().toUTC().toRFC2822()}</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>
${rssItems.join("\n")}
</channel>
</rss>`;
}

View file

@ -0,0 +1,13 @@
export function getPopularPosts(posts, analytics) {
const filteredPosts = posts.filter((post) =>
analytics.some((p) => p.page.includes(post.url))
);
const sortedPosts = filteredPosts.sort((a, b) => {
const visitors = (page) =>
analytics.find((p) => p.page.includes(page.url))?.visitors || 0;
return visitors(b) - visitors(a);
});
return sortedPosts;
}