feat: numerous other pages
This commit is contained in:
parent
159b60b3fb
commit
ca34a11ad4
54 changed files with 1074 additions and 101 deletions
1
.env
1
.env
|
@ -1,4 +1,5 @@
|
|||
ACCOUNT_ID_PLEX=
|
||||
API_KEY_PLAUSIBLE=
|
||||
SUPABASE_URL=
|
||||
SUPABASE_KEY=
|
||||
CF_ACCOUNT_ID=
|
||||
|
|
10
_redirects
10
_redirects
|
@ -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
|
||||
|
|
|
@ -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
76
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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} />
|
||||
)}
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import NavLink from './nav/NavLink.astro';
|
||||
import NavLink from '@components/nav/NavLink.astro';
|
||||
|
||||
const { nav, updated } = Astro.props;
|
||||
---
|
||||
|
|
|
@ -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 === '/';
|
||||
|
|
155
src/components/Metadata.astro
Normal file
155
src/components/Metadata.astro
Normal 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) =>
|
||||
({
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
})[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"
|
||||
/>
|
64
src/components/blocks/BlockRenderer.astro
Normal file
64
src/components/blocks/BlockRenderer.astro
Normal 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} />
|
||||
)}
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="mastodon-post">
|
||||
<p>{post.content}</p>
|
||||
</article>
|
|
@ -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();
|
||||
|
|
|
@ -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 />
|
15
src/components/blocks/banners/Mastodon.astro
Normal file
15
src/components/blocks/banners/Mastodon.astro
Normal 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>
|
10
src/components/blocks/links/AddonLinks.astro
Normal file
10
src/components/blocks/links/AddonLinks.astro
Normal 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>
|
21
src/components/blocks/links/PopularPosts.astro
Normal file
21
src/components/blocks/links/PopularPosts.astro
Normal 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>
|
||||
)}
|
26
src/components/blocks/links/RecentLinks.astro
Normal file
26
src/components/blocks/links/RecentLinks.astro
Normal 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>
|
||||
)}
|
|
@ -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();
|
70
src/components/media/Grid.astro
Normal file
70
src/components/media/Grid.astro
Normal 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} />
|
||||
)}
|
9
src/components/media/ProgressBar.astro
Normal file
9
src/components/media/ProgressBar.astro
Normal 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>
|
||||
)}
|
29
src/components/media/music/Chart.astro
Normal file
29
src/components/media/music/Chart.astro
Normal 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>
|
41
src/components/media/music/Recent.astro
Normal file
41
src/components/media/music/Recent.astro
Normal 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>
|
18
src/components/media/watching/Hero.astro
Normal file
18
src/components/media/watching/Hero.astro
Normal 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
1
src/env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
|
@ -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();
|
||||
|
|
46
src/pages/.well-known/webfinger.js
Normal file
46
src/pages/.well-known/webfinger.js
Normal 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 });
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
22
src/pages/feeds/json/all.json.js
Normal file
22
src/pages/feeds/json/all.json.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/json/books.json.js
Normal file
22
src/pages/feeds/json/books.json.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/json/links.json.js
Normal file
22
src/pages/feeds/json/links.json.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/json/movies.json.js
Normal file
22
src/pages/feeds/json/movies.json.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/json/posts.json.js
Normal file
22
src/pages/feeds/json/posts.json.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/rss/all.xml.js
Normal file
22
src/pages/feeds/rss/all.xml.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/rss/books.xml.js
Normal file
22
src/pages/feeds/rss/books.xml.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/rss/links.xml.js
Normal file
22
src/pages/feeds/rss/links.xml.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/rss/movies.xml.js
Normal file
22
src/pages/feeds/rss/movies.xml.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
22
src/pages/feeds/rss/posts.xml.js
Normal file
22
src/pages/feeds/rss/posts.xml.js
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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
29
src/pages/humans.txt.js
Normal 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 });
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
24
src/pages/music/releases.ics.js
Normal file
24
src/pages/music/releases.ics.js
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -55,5 +55,5 @@ const pagination = {
|
|||
</article>
|
||||
))}
|
||||
|
||||
<Paginator pagination={pagination} appVersion="1.0.0" />
|
||||
<Paginator pagination={pagination} />
|
||||
</Layout>
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { fetchAllRobots } from '../utils//data/robots.js';
|
||||
import { fetchAllRobots } from '@utils//data/robots.js';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
38
src/utils/albumReleasesCalendar.js
Normal file
38
src/utils/albumReleasesCalendar.js
Normal 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;
|
||||
}
|
34
src/utils/data/analytics.js
Normal file
34
src/utils/data/analytics.js
Normal 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 [];
|
||||
}
|
||||
}
|
39
src/utils/generateJsonFeed.js
Normal file
39
src/utils/generateJsonFeed.js
Normal 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);
|
||||
}
|
45
src/utils/generateRssFeed.js
Normal file
45
src/utils/generateRssFeed.js
Normal 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>`;
|
||||
}
|
13
src/utils/getPopularPosts.js
Normal file
13
src/utils/getPopularPosts.js
Normal 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;
|
||||
}
|
Reference in a new issue