diff --git a/.env b/.env
index 706bf39..a999c62 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,5 @@
ACCOUNT_ID_PLEX=
+API_KEY_PLAUSIBLE=
SUPABASE_URL=
SUPABASE_KEY=
CF_ACCOUNT_ID=
diff --git a/_redirects b/_redirects
index 6b2361f..6e0cb07 100644
--- a/_redirects
+++ b/_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
diff --git a/astro.config.mjs b/astro.config.mjs
index 1b19b5b..0e37b3b 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -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",
diff --git a/package-lock.json b/package-lock.json
index c9e75be..3ad6f3a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 2606701..4a4aa90 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/BlockRenderer.astro b/src/components/BlockRenderer.astro
deleted file mode 100644
index 17e391d..0000000
--- a/src/components/BlockRenderer.astro
+++ /dev/null
@@ -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' && (
-
-)}
-
-{block.type === 'hero' && (
-
-)}
-
-{block.type === 'markdown' && (
-
-)}
-
-{block.type === 'modal' && (
-
-)}
-
-{block.type === 'associated_media' && (
-
-)}
-
-{block.type === 'github_banner' && (
-
-)}
\ No newline at end of file
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 13f4c49..78720e1 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -1,5 +1,5 @@
---
-import NavLink from './nav/NavLink.astro';
+import NavLink from '@components/nav/NavLink.astro';
const { nav, updated } = Astro.props;
---
diff --git a/src/components/Header.astro b/src/components/Header.astro
index 7ed6a85..a1c48f4 100644
--- a/src/components/Header.astro
+++ b/src/components/Header.astro
@@ -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 === '/';
diff --git a/src/components/Metadata.astro b/src/components/Metadata.astro
new file mode 100644
index 0000000..f93a169
--- /dev/null
+++ b/src/components/Metadata.astro
@@ -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]
+ ) || "";
+---
+
+
+
+
+{pageTitle}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/blocks/BlockRenderer.astro b/src/components/blocks/BlockRenderer.astro
new file mode 100644
index 0000000..c182293
--- /dev/null
+++ b/src/components/blocks/BlockRenderer.astro
@@ -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' && (
+
+)}
+
+{block.type === 'associated_media' && (
+
+)}
+
+{block.type === 'divider' && (
+
+)}
+
+{block.type === 'github_banner' && (
+
+)}
+
+{block.type === 'hero' && (
+
+)}
+
+{block.type === 'markdown' && (
+
+)}
+
+{block.type === 'npm_banner' && (
+
+)}
+
+{block.type === 'modal' && (
+
+)}
+
+{block.type === 'rss_banner' && (
+
+)}
+
+{block.type === 'youtube_player' && (
+
+)}
\ No newline at end of file
diff --git a/src/components/blocks/MastodonPost.astro b/src/components/blocks/MastodonPost.astro
deleted file mode 100644
index 06baf3e..0000000
--- a/src/components/blocks/MastodonPost.astro
+++ /dev/null
@@ -1,7 +0,0 @@
----
-const { post } = Astro.props;
----
-
-
- {post.content}
-
\ No newline at end of file
diff --git a/src/components/blocks/NowPlaying.astro b/src/components/blocks/NowPlaying.astro
index cd629af..cfcaf08 100644
--- a/src/components/blocks/NowPlaying.astro
+++ b/src/components/blocks/NowPlaying.astro
@@ -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();
diff --git a/src/components/blocks/YouTubePlayer.astro b/src/components/blocks/YouTubePlayer.astro
index 98339c1..1040ecd 100644
--- a/src/components/blocks/YouTubePlayer.astro
+++ b/src/components/blocks/YouTubePlayer.astro
@@ -1,10 +1,7 @@
---
+import YoutubeVideo from '@npm/youtube-video-element/dist/react.js';
+
const { url } = Astro.props;
---
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/components/blocks/banners/Mastodon.astro b/src/components/blocks/banners/Mastodon.astro
new file mode 100644
index 0000000..275a536
--- /dev/null
+++ b/src/components/blocks/banners/Mastodon.astro
@@ -0,0 +1,15 @@
+---
+import { IconBrandMastodon } from "@tabler/icons-react";
+
+const { url } = Astro.props;
+---
+
diff --git a/src/components/blocks/links/AddonLinks.astro b/src/components/blocks/links/AddonLinks.astro
new file mode 100644
index 0000000..cdd04d4
--- /dev/null
+++ b/src/components/blocks/links/AddonLinks.astro
@@ -0,0 +1,10 @@
+---
+import PopularPosts from './PopularPosts.astro';
+import RecentLinks from './RecentLinks.astro';
+
+const { popularPosts, links } = Astro.props;
+---
+
\ No newline at end of file
diff --git a/src/components/blocks/links/PopularPosts.astro b/src/components/blocks/links/PopularPosts.astro
new file mode 100644
index 0000000..e52f5bc
--- /dev/null
+++ b/src/components/blocks/links/PopularPosts.astro
@@ -0,0 +1,21 @@
+---
+const { popularPosts } = Astro.props;
+import { IconChartBarPopular } from '@tabler/icons-react';
+---
+{popularPosts && popularPosts.length > 0 && (
+
+
+
+ {popularPosts.slice(0, 5).map((post) => (
+ -
+ {post.title}
+
+ ))}
+
+
+)}
diff --git a/src/components/blocks/links/RecentLinks.astro b/src/components/blocks/links/RecentLinks.astro
new file mode 100644
index 0000000..71930cf
--- /dev/null
+++ b/src/components/blocks/links/RecentLinks.astro
@@ -0,0 +1,26 @@
+---
+const { links } = Astro.props;
+import { IconLink } from '@tabler/icons-react';
+---
+{links && links.length > 0 && (
+
+
+
+
+)}
diff --git a/src/components/Intro.astro b/src/components/home/Intro.astro
similarity index 100%
rename from src/components/Intro.astro
rename to src/components/home/Intro.astro
diff --git a/src/components/RecentActivity.astro b/src/components/home/RecentActivity.astro
similarity index 100%
rename from src/components/RecentActivity.astro
rename to src/components/home/RecentActivity.astro
diff --git a/src/components/RecentPosts.astro b/src/components/home/RecentPosts.astro
similarity index 93%
rename from src/components/RecentPosts.astro
rename to src/components/home/RecentPosts.astro
index 5b19e0e..a222d3d 100644
--- a/src/components/RecentPosts.astro
+++ b/src/components/home/RecentPosts.astro
@@ -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();
diff --git a/src/components/media/Grid.astro b/src/components/media/Grid.astro
new file mode 100644
index 0000000..9292eda
--- /dev/null
+++ b/src/components/media/Grid.astro
@@ -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 };
+}
+---
+
+
+
+{!hidePagination && (
+
+)}
diff --git a/src/components/media/ProgressBar.astro b/src/components/media/ProgressBar.astro
new file mode 100644
index 0000000..1c9799e
--- /dev/null
+++ b/src/components/media/ProgressBar.astro
@@ -0,0 +1,9 @@
+---
+const { percentage } = Astro.props;
+---
+
+{percentage && (
+
+)}
\ No newline at end of file
diff --git a/src/components/media/music/Chart.astro b/src/components/media/music/Chart.astro
new file mode 100644
index 0000000..b4670f3
--- /dev/null
+++ b/src/components/media/music/Chart.astro
@@ -0,0 +1,29 @@
+---
+import ProgressBar from '@components/media/ProgressBar.astro';
+
+const { data, count } = Astro.props;
+---
+
+
+
+ {data.slice(0, count).map((item) => {
+ const percentage = `${item.chart.percentage}%`;
+ const playsLabel = item.chart.plays === 1 ? 'play' : 'plays';
+
+ return (
+ -
+
+
+ );
+ })}
+
+
\ No newline at end of file
diff --git a/src/components/media/music/Recent.astro b/src/components/media/music/Recent.astro
new file mode 100644
index 0000000..9921178
--- /dev/null
+++ b/src/components/media/music/Recent.astro
@@ -0,0 +1,41 @@
+---
+const { data, globals } = Astro.props;
+---
+
+
+ {data.slice(0, 10).map((item) => (
+
+
+
+
+ ))}
+
diff --git a/src/components/media/watching/Hero.astro b/src/components/media/watching/Hero.astro
new file mode 100644
index 0000000..078ef55
--- /dev/null
+++ b/src/components/media/watching/Hero.astro
@@ -0,0 +1,18 @@
+---
+import Hero from "@components/blocks/Hero.astro";
+
+const { movie, globals } = Astro.props;
+---
+
+
+
+
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..9bc5cb4
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index d7fe958..cbb9e18 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -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();
diff --git a/src/pages/.well-known/webfinger.js b/src/pages/.well-known/webfinger.js
new file mode 100644
index 0000000..cddfeea
--- /dev/null
+++ b/src/pages/.well-known/webfinger.js
@@ -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 });
+ }
+}
diff --git a/src/pages/[permalink].astro b/src/pages/[permalink].astro
index 2378f13..06faa28 100644
--- a/src/pages/[permalink].astro
+++ b/src/pages/[permalink].astro
@@ -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';
diff --git a/src/pages/feeds/json/all.json.js b/src/pages/feeds/json/all.json.js
new file mode 100644
index 0000000..6cc1a4d
--- /dev/null
+++ b/src/pages/feeds/json/all.json.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/json/books.json.js b/src/pages/feeds/json/books.json.js
new file mode 100644
index 0000000..54e2a61
--- /dev/null
+++ b/src/pages/feeds/json/books.json.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/json/links.json.js b/src/pages/feeds/json/links.json.js
new file mode 100644
index 0000000..98fdd5f
--- /dev/null
+++ b/src/pages/feeds/json/links.json.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/json/movies.json.js b/src/pages/feeds/json/movies.json.js
new file mode 100644
index 0000000..852eeea
--- /dev/null
+++ b/src/pages/feeds/json/movies.json.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/json/posts.json.js b/src/pages/feeds/json/posts.json.js
new file mode 100644
index 0000000..ea6e978
--- /dev/null
+++ b/src/pages/feeds/json/posts.json.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/rss/all.xml.js b/src/pages/feeds/rss/all.xml.js
new file mode 100644
index 0000000..77bac08
--- /dev/null
+++ b/src/pages/feeds/rss/all.xml.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/rss/books.xml.js b/src/pages/feeds/rss/books.xml.js
new file mode 100644
index 0000000..187c346
--- /dev/null
+++ b/src/pages/feeds/rss/books.xml.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/rss/links.xml.js b/src/pages/feeds/rss/links.xml.js
new file mode 100644
index 0000000..e2219cd
--- /dev/null
+++ b/src/pages/feeds/rss/links.xml.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/rss/movies.xml.js b/src/pages/feeds/rss/movies.xml.js
new file mode 100644
index 0000000..78a314d
--- /dev/null
+++ b/src/pages/feeds/rss/movies.xml.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/rss/posts.xml.js b/src/pages/feeds/rss/posts.xml.js
new file mode 100644
index 0000000..53f9890
--- /dev/null
+++ b/src/pages/feeds/rss/posts.xml.js
@@ -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",
+ },
+ });
+}
diff --git a/src/pages/feeds/syndication.xml.js b/src/pages/feeds/rss/syndication.xml.js
similarity index 93%
rename from src/pages/feeds/syndication.xml.js
rename to src/pages/feeds/rss/syndication.xml.js
index 2d704bc..b75af08 100644
--- a/src/pages/feeds/syndication.xml.js
+++ b/src/pages/feeds/rss/syndication.xml.js
@@ -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();
diff --git a/src/pages/humans.txt.js b/src/pages/humans.txt.js
new file mode 100644
index 0000000..abe30cf
--- /dev/null
+++ b/src/pages/humans.txt.js
@@ -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 });
+ }
+}
diff --git a/src/pages/index.astro b/src/pages/index.astro
index d8a2a25..51d8289 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -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';
diff --git a/src/pages/music/releases.ics.js b/src/pages/music/releases.ics.js
new file mode 100644
index 0000000..3b2e2b0
--- /dev/null
+++ b/src/pages/music/releases.ics.js
@@ -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,
+ });
+ }
+}
diff --git a/src/pages/posts/[...page].astro b/src/pages/posts/[...page].astro
index 395d6e1..93404b2 100644
--- a/src/pages/posts/[...page].astro
+++ b/src/pages/posts/[...page].astro
@@ -55,5 +55,5 @@ const pagination = {
))}
-
+
\ No newline at end of file
diff --git a/src/pages/posts/[year]/[title].astro b/src/pages/posts/[year]/[title].astro
index 78a0c56..1a298f2 100644
--- a/src/pages/posts/[year]/[title].astro
+++ b/src/pages/posts/[year]/[title].astro
@@ -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) => )
}
-
+ {post.mastodon_url && }
+
diff --git a/src/pages/robots.txt.js b/src/pages/robots.txt.js
index fbc6036..6c26b13 100644
--- a/src/pages/robots.txt.js
+++ b/src/pages/robots.txt.js
@@ -1,4 +1,4 @@
-import { fetchAllRobots } from '../utils//data/robots.js';
+import { fetchAllRobots } from '@utils//data/robots.js';
export async function GET() {
try {
diff --git a/src/styles/components/banners.css b/src/styles/components/banners.css
index 117ae9b..9a2cca4 100644
--- a/src/styles/components/banners.css
+++ b/src/styles/components/banners.css
@@ -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);
}
diff --git a/src/styles/components/mastodon-post.css b/src/styles/components/mastodon-post.css
deleted file mode 100644
index d6f794f..0000000
--- a/src/styles/components/mastodon-post.css
+++ /dev/null
@@ -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);
- }
- }
- }
- }
-}
diff --git a/src/styles/index.css b/src/styles/index.css
index 38205f9..8bee591 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -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);
diff --git a/src/utils/albumReleasesCalendar.js b/src/utils/albumReleasesCalendar.js
new file mode 100644
index 0000000..b67ec7c
--- /dev/null
+++ b/src/utils/albumReleasesCalendar.js
@@ -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;
+}
diff --git a/src/utils/data/analytics.js b/src/utils/data/analytics.js
new file mode 100644
index 0000000..0f17e30
--- /dev/null
+++ b/src/utils/data/analytics.js
@@ -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 [];
+ }
+}
diff --git a/src/utils/generateJsonFeed.js b/src/utils/generateJsonFeed.js
new file mode 100644
index 0000000..7c58c3a
--- /dev/null
+++ b/src/utils/generateJsonFeed.js
@@ -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);
+}
diff --git a/src/utils/generateRssFeed.js b/src/utils/generateRssFeed.js
new file mode 100644
index 0000000..f0c347f
--- /dev/null
+++ b/src/utils/generateRssFeed.js
@@ -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 `
+ -
+
+ ${entryFeed.url}
+ ${DateTime.fromISO(entryFeed.date).toRFC2822()}
+ ${entryFeed.url}
+ ${
+ entryFeed.image
+ ? ``
+ : ""
+ }
+
+
`;
+ });
+
+ return `
+
+
+
+
+
+
+ ${globals.url}${permalink}
+ ${DateTime.now().toUTC().toRFC2822()}
+
+
+ ${globals.url}${permalink}
+ ${globals.cdn_url}${globals.avatar}?class=w200
+ 144
+ 144
+
+ ${rssItems.join("\n")}
+
+`;
+}
diff --git a/src/utils/getPopularPosts.js b/src/utils/getPopularPosts.js
new file mode 100644
index 0000000..7e33805
--- /dev/null
+++ b/src/utils/getPopularPosts.js
@@ -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;
+}