feat: initial commit

This commit is contained in:
Cory Dransfeldt 2024-11-16 16:43:07 -08:00
commit 0ff7457679
No known key found for this signature in database
192 changed files with 24379 additions and 0 deletions

7
.env Normal file
View file

@ -0,0 +1,7 @@
ACCOUNT_ID_PLEX=
API_KEY_PLAUSIBLE=
SUPABASE_URL=
SUPABASE_KEY=
CF_ACCOUNT_ID=
CF_ZONE_ID=
RSS_TO_MASTODON_KV_NAMESPACE_ID=

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
github: cdransf
buy_me_a_coffee: cory

6
.github/dependabot.yaml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'

27
.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# build output
dist/
# workers
wrangler.toml
.wrangler
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env.local
# macOS-specific files
.DS_Store
# VS Code files
.vscode/

8
.markdownlint.json Normal file
View file

@ -0,0 +1,8 @@
{
"default": true,
"MD009": false,
"MD013": false,
"MD033": false,
"MD041": false,
"MD047": false
}

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
save-exact=true
cache=~/.npm

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
22

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Cory Dransfeldt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# coryd.dev
Hi! I'm Cory. 👋🏻
This is the code for my personal website and portfolio. Built using [Astro](https://astro.build) and [other tools](https://coryd.dev/colophon).
- [Follow me on Mastodon](https://follow.coryd.dev/@cory)
- [Buy me a coffee](https://buymeacoffee.com/cory)
- [What I'm listening to](https://coryd.dev/music)
- [What I'm watching](https://coryd.dev/watching)
- [What I'm reading](https://coryd.dev/books)
- [What I'm doing now](https://coryd.dev/now)
---
## Local dev setup
1. `npm install`
## Local dev workflow
1. `npm start`
2. Open `http://localhost:4321`
### Other commands
`npm run build`: builds Astro output.
`npm run prevew`: previews built Astro output (uses wrangler command compatible with Cloudflare adapter under the hood).
`npm run update:deps`: checks for dependency updates and updates Astro.
`npm run build:worker -- WORKER_NAME`: builds the `wrangler.toml` file for the named worker.
`npm run deploy:worker --worker=WORKER_NAME`: deploys the named worker.
## Required environment variables
```plaintext
ACCOUNT_ID_PLEX
API_KEY_PLAUSIBLE
SUPABASE_URL
SUPABASE_KEY
CF_ACCOUNT_ID
CF_ZONE_ID
RSS_TO_MASTODON_KV_NAMESPACE_ID
```

69
astro.config.mjs Normal file
View file

@ -0,0 +1,69 @@
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://coryd.dev",
output: "server",
adapter: cloudflare(),
integrations: [sitemap()],
vite: {
build: {
minify: "terser",
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
ecma: 2020,
passes: 3,
module: true,
toplevel: true,
},
mangle: {
safari10: true,
properties: {
regex: /^_/,
},
},
format: {
comments: false,
ascii_only: true,
},
},
sourcemap: false,
rollupOptions: {
treeshake: true,
external: ["/js/script.js"],
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
if (id.includes("@supabase")) return "supabase-vendor";
if (id.includes("highlight.js")) return "highlight-vendor";
}
},
},
},
},
optimizeDeps: {
include: [
"@astrojs/sitemap",
"date-fns",
"highlight.js",
"minisearch",
"@supabase/supabase-js",
"@cdransf/astro-tabler-icons",
],
},
resolve: {
alias: {
"@components": "/src/components",
"@data": "/src/utils/data",
"@layouts": "/src/layouts",
"@npm": "/node_modules",
"@scripts": "/src/scripts",
"@styles": "/src/styles",
"@utils": "/src/utils",
},
},
},
});

9087
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "coryd.dev",
"type": "module",
"version": "1.1.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "wrangler pages dev ./dist",
"astro": "astro",
"clean": "rimraf dist",
"update:deps": "npm upgrade && ncu && npx @astrojs/upgrade && npm i",
"build:worker": "node scripts/worker-build.mjs $WORKER_NAME",
"deploy:worker": "wrangler deploy --env production --config workers/$npm_config_worker/wrangler.toml"
},
"dependencies": {
"@astrojs/check": "0.9.4",
"@astrojs/cloudflare": "^11.2.0",
"@astrojs/rss": "4.0.9",
"@astrojs/sitemap": "3.2.1",
"@cdransf/astro-tabler-icons": "1.0.5",
"@supabase/supabase-js": "^2.46.1",
"astro": "4.16.14",
"astro-embed": "0.9.0",
"date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
"highlight.js": "11.10.0",
"markdown-it": "14.1.0",
"markdown-it-anchor": "9.2.0",
"markdown-it-footnote": "4.0.0",
"minisearch": "7.1.1",
"truncate-html": "1.1.2",
"typescript": "5.6.3",
"youtube-video-element": "^1.1.6"
},
"devDependencies": {
"dotenv-flow": "4.1.0",
"fast-xml-parser": "4.5.0",
"html-to-text": "9.0.5",
"i18n-iso-countries": "7.13.0",
"ics": "^3.8.1",
"rimraf": "6.0.1",
"slugify": "1.6.6",
"terser": "5.36.0"
}
}

45
public/_headers Normal file
View file

@ -0,0 +1,45 @@
/feeds/all.xml
Content-Type: application/xml; charset=utf-8
x-content-type-options: nosniff
/feeds/books.xml
Content-Type: application/xml; charset=utf-8
x-content-type-options: nosniff
/feeds/links.xml
Content-Type: application/xml; charset=utf-8
x-content-type-options: nosniff
/feeds/posts.xml
Content-Type: application/xml; charset=utf-8
x-content-type-options: nosniff
/feeds/movies.xml
Content-Type: application/xml; charset=utf-8
x-content-type-options: nosniff
/feeds/syndication.xml
Content-Type: application/xml; charset=utf-8
x-content-type-options: nosniff
/api/search
Content-Type: application/json
/.well-known/webfinger
Content-Type: application/jrd+json; charset=utf-8
/blogroll.opml
Content-Disposition: attachment; filename=cory-dransfeldt-blogroll.opml
/music/releases.ics
Content-Type: text/calendar
Cache-Control: public, max-age=0, must-revalidate
Access-Control-Allow-Origin: *
Content-Disposition: inline; filename="releases.ics"
/*
Content-Security-Policy: upgrade-insecure-requests; block-all-mixed-content;
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin, no-referrer-when-downgrade
Permissions-Policy: autoplay=(), camera=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=()

83
public/_redirects Normal file
View file

@ -0,0 +1,83 @@
# 404s
/now.html /now 301
/contact.html /contact 301
# feeds
/feed.xml /feeds/posts 301
/follow.xml /feeds/all 301
/feeds/posts /feeds/posts.xml 301
/feeds/posts.json /feeds/posts.xml 301
/feeds/links /feeds/links.xml 301
/feeds/links.json /feeds/links.xml 301
/feeds/books /feeds/books.xml 301
/feeds/books.json /feeds/books.xml 301
/feeds/movies /feeds/movies.xml 301
/feeds/movies.json /feeds/movies.xml 301
/feeds/all /feeds/all.xml 301
/feeds/all.json /feeds/all.xml 301
/feeds/posts/ /feeds/posts 301
/feeds/links/ /feeds/links 301
/feeds/books/ /feeds/books 301
/feeds/movies/ /feeds/movies 301
/feeds/all/ /feeds/all 301
# general
/articles/ /posts/ 301
/tags /search 301
/sitemap.xml /sitemap-index.xml 301
/watching/favorite-movies /watching/favorites/movies 301
/watching/favorite-movies/ /watching/favorites/movies 301
/watching/favorite-shows /watching/favorites/shows 301
/watching/favorite-shows/ /watching/favorites/shows 301
# blog posts
/blog/fixing-safari-icloud-syncing /posts/2022/fixing-safari-icloud-syncing/ 301
/blog/migrating-to-fastmail /posts/2022/migrating-to-fastmail/ 301
/blog/client-side-webmentions-in-nextjs /posts/2023/client-side-webmentions-in-nextjs/ 301
/blog/apple-centric-digital-privacy-tools /posts/2022/apple-centric-digital-privacy-tools/ 301
/blog/automating-rss-syndication-with-nextjs-github /posts/2023/automating-rss-syndication-with-nextjs-github/ 301
/blog/apple-music-a-tale-of-woe /posts/2021/apple-music-a-tale-of-woe/ 301
/blog/adding-client-side-rendered-webmentions-to-my-blog /posts/2023/client-side-webmentions-in-nextjs/ 301
/blog/automating-email-cleanup-in-gmail /posts/2022/automating-email-cleanup-in-gmail/ 301
/blog/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt /posts/2023/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt/ 301
/blog/simple-api-fetch-hooks-with-swr /posts/2022/simple-api-fetch-hooks-with-swr/ 301
/2023/02/automatingandprobablyoverengineeringmy-nowpage /posts/2023/automating-and-overengineering-my-now-page/ 301
/2023/01/workflows-handling-inbound-email-on-fastmail-with-regular-expressions /posts/2023/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt/ 301
/posts/2023/i-block-ads/null /posts/2023/i-block-ads/ 301
/posts/2023/i-dont-want-streaming-music/null /posts/2023/i-dont-want-streaming-music/ 301
/posts/2022/migrating-to-fastmail/null /posts/2022/migrating-to-fastmail/ 301
/posts/2023/webmentions-in-eleventy/null /posts/2023/webmentions-in-eleventy/ 301
/posts/2023/a-safari-specific-guide-to-making-the-modern-web-suck-less/null /posts/2023/a-safari-specific-guide-to-making-the-modern-web-suck-less/ 301
/posts/2024/dont-be-afraid-to-admit-when-you-dont-know-something/ /posts/2024/dont-be-afraid-to-admin-when-you-dont-know-something/ 301
/posts/2024/data-sharing-should-always-be-opt-in/ /posts/2024/access-to-data-isnt-a-grant-to-exploit-it/ 301
/posts/2023/popular-posts-widget-using-eleventy-plausible/ /posts/2023/building-a-popular-posts-widget-in-eleventy-using-plausible-analytics/ 301
/posts/2023/i-dont-want-streaming-music/ /posts/2023/i-dont-want-streaming-music-i-just-want-to-stream-my-music/ 301
/posts/2021/apple-music-a-tale-of-woe/ /posts/2022/apple-music-a-tale-of-woe/ 301
/posts/2024/weaving-music-in-and-out-of-my-personal-site/ /posts/2024/weaving-music-data-in-and-out-of-my-personal-website/ 301
/posts/2023/fastmail-handling-inbound-email-with-regex-filters-now-with-chatgpt/ /posts/2023/workflows-handling-inbound-email-on-fastmail-with-regular-expressions-now-featuring-chatgpt/ 301
/-want-anything-your-ai-generates/ /posts/2024/i-dont-want-anything-your-ai-generates/ 301
/posts/2023/default-apps-2023/ /posts/2023/my-default-apps-2023-edition/ 301
/posts/2024/dont-be-afraid-to-admin-when-you-dont-know-something/ /posts/2024/dont-be-afraid-to-admin-when-you-dont-know-something/ 301
/posts/2023/displaying-listening-data-from-apple-music-using-musickit/ /posts/2023/displaying-listening-data-from-apple-music-using-musickitjs/ 301
/posts/2024/2024-minimalism-as-self-preservation/ /posts/2024/minimalism-as-self-preservation/ 301
/posts/2023/client-side-webmentions-in-nextjs/ /posts/2023/adding-client-side-webmentions-to-my-nextjs-blog/ 301
/posts/2024/2024-adblocker-required/ /posts/2024/adblocker-required/ 301
/posts/2023/scheduled-eleventy-builds-cron-github-actions/ /posts/2023/scheduled-eleventy-builds-on-vercel-with-cron-triggered-github-actions/ 301
/posts/2024/against-the-commercial-web/ /posts/2024/against-the-commercial-internet/ 301
/posts/2023/i-removed-tailwind-from-my-site/ /posts/2024/i-removed-tailwind-from-my-site/
/posts/2024/handling-images-with-b2-netlify-image-cdn-hazel-mountain-duck/ /posts/2024/handling-images-with-b2-netlifys-image-cdn-hazel-and-mountain-duck/ 301
/posts/2023/my-default-apps-2023-edition/ /uses 301
/posts/2024/link-blogging-using-readwise/ /posts/2024/link-blogging-using-readwise-reader/ 301
/2022/12/automating-email-cleanup-in-gmail /posts/2022/automating-email-cleanup-in-gmail/ 301
/posts/2023/automate-syndicate-content-mastodon-eleventy/ /posts/2023/automate-and-syndicate-content-from-eleventy-to-mastodon/ 301
/posts/2023/road-to-madness-apple-music-charts/ /posts/2023/road-to-madness-charting-apple-music-listening-data/ 301
/posts/2023/semi-automated-hashtags-syndicated-posts/ /posts/2023/semi-automated-hashtags-for-syndicated-posts/ 301
/posts/2023/automating-rss-syndication-with-nextjs-github/ /posts/2023/automating-rss-syndication-and-sharing-with-nextjs-and-github/ 301
/posts/2023/locally-stored-music-and-storage-as-a-meaningful-constraint/ /posts/2023/doppler-locally-stored-music-and-storage-as-a-beneficial-constraint/ 301
/blog/digital-privacy-tools /posts/2021/digital-privacy-tools/ 301
/posts/2023/now-page-update-matter-favorites/ /posts/2023/now-page-update-favorite-articles-from-matter/ 301
/posts/2023/now-playing-eleventy-netlify-edge-functions-emoji/ /posts/2023/displaying-now-playing-data-with-matching-emoji-using-netlify-edge-functions-and-eleventy/ 301
/posts/2014/sublime-text-ctrl-tab-key-bindings/ /posts/2014/sublime-text-3-ctrl-tab-key-bindings/ 301
/posts/2022/simple-api-fetch-hooks-with-swr/ /posts/2022/simple-data-fetching-with-custom-react-hooks-and-swr/ 301
/posts/2023/drying-up-now-page-templates-eleventy/ /posts/2023/drying-up-now-page-templates-and-normalizing-data-in-eleventy/ 301

5340
public/feeds/style.css Normal file

File diff suppressed because it is too large Load diff

78
public/feeds/style.xsl Normal file
View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title><xsl:value-of select="/rss/channel/title" /> Web Feed</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="stylesheet" href="/feeds/style.css" type="text/css" />
</head>
<body class="bg-white">
<nav class="container-md px-3 py-2 mt-2 mt-md-5 mb-5 markdown-body">
<p class="bg-yellow-light ml-n1 px-1 py-1 mb-1">
<strong>This is a web feed,</strong> also known as an RSS feed. <strong>Subscribe</strong>
by copying the URL from the address bar into your newsreader. </p>
<p class="text-gray"> Visit <a href="https://aboutfeeds.com">About Feeds</a> to get
started with newsreaders and subscribing. Its free. </p>
</nav>
<div class="container-md px-3 py-3 markdown-body">
<header class="py-5">
<h1 class="border-0">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
style="vertical-align: text-bottom; width: 1.2em; height: 1.2em;" class="pr-1"
id="RSSicon" viewBox="0 0 256 256">
<defs>
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
<stop offset="0.0" stop-color="#E3702D" />
<stop offset="0.1071" stop-color="#EA7D31" />
<stop offset="0.3503" stop-color="#F69537" />
<stop offset="0.5" stop-color="#FB9E3A" />
<stop offset="0.7016" stop-color="#EA7C31" />
<stop offset="0.8866" stop-color="#DE642B" />
<stop offset="1.0" stop-color="#D95B29" />
</linearGradient>
</defs>
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15" />
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52" />
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)" />
<circle cx="68" cy="189" r="24" fill="#FFF" />
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF" />
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF" />
</svg>
Web Feed Preview </h1>
<h2>
<xsl:value-of select="/rss/channel/title" />
</h2>
<p>
<xsl:value-of select="/rss/channel/description" />
</p>
<a class="head_link" target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="/rss/channel/link" />
</xsl:attribute> Visit
Website &#x2192; </a>
</header>
<h2>Recent Items</h2>
<xsl:for-each select="/rss/channel/item">
<div class="pb-5">
<h3 class="mb-0">
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="link" />
</xsl:attribute>
<xsl:value-of select="title" />
</a>
</h3>
<small class="text-gray"> Published: <xsl:value-of select="pubDate" />
</small>
</div>
</xsl:for-each>
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

BIN
public/fonts/ml.woff2 Normal file

Binary file not shown.

BIN
public/fonts/mlb.woff2 Normal file

Binary file not shown.

BIN
public/fonts/mlbi.woff2 Normal file

Binary file not shown.

BIN
public/fonts/mli.woff2 Normal file

Binary file not shown.

105
public/scripts/index.js Normal file
View file

@ -0,0 +1,105 @@
window.addEventListener("load", () => {
// menu keyboard controls
(() => {
const menuInput = document.getElementById("menu-toggle");
const menuButtonContainer = document.querySelector(
".menu-button-container"
);
const menuItems = document.querySelectorAll(".menu-primary li");
menuButtonContainer.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
menuInput.checked = !menuInput.checked;
}
});
menuItems.forEach((item) => {
item.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
item.querySelector("a").click();
}
});
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && menuInput.checked) menuInput.checked = false;
});
})();
// modal keyboard controls and scroll management
(() => {
const modalInputs = document.querySelectorAll(".modal-input");
if (!modalInputs) return;
const toggleBodyScroll = (disableScroll) => {
if (disableScroll) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
};
const checkModals = () => {
let isAnyModalOpen = false;
modalInputs.forEach((modalInput) => {
if (modalInput.checked) isAnyModalOpen = true;
});
toggleBodyScroll(isAnyModalOpen);
};
modalInputs.forEach((modalInput) => {
modalInput.addEventListener("change", checkModals);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
modalInputs.forEach((modalInput) => {
if (modalInput.checked) modalInput.checked = false;
});
toggleBodyScroll(false);
}
});
checkModals();
})();
// text toggle for media pages
(() => {
const button = document.querySelector("[data-toggle-button]");
const content = document.querySelector("[data-toggle-content]");
const text = document.querySelectorAll("[data-toggle-content] p");
const minHeight = 500; // this needs to match the height set on [data-toggle-content].text-toggle-hidden in text-toggle.css
const interiorHeight = Array.from(text).reduce(
(acc, node) => acc + node.scrollHeight,
0
);
if (!button || !content || !text) return;
if (interiorHeight < minHeight) {
content.classList.remove("text-toggle-hidden");
button.style.display = "none";
return;
}
button.addEventListener("click", () => {
const isHidden = content.classList.toggle("text-toggle-hidden");
button.textContent = isHidden ? "Show more" : "Show less";
});
})();
// pagination
(() => {
const dropdown = document.querySelector(".pagination select.client-side");
if (dropdown)
dropdown.addEventListener("change", (event) => {
const selectedOption = event.target.options[event.target.selectedIndex];
const selectedHref = selectedOption.getAttribute("data-href");
if (selectedHref) window.location.href = selectedHref;
});
})();
});

View file

@ -0,0 +1,43 @@
CREATE OR REPLACE FUNCTION public.search_optimized_index(search_query text, page_size integer, page_offset integer, types text[])
RETURNS TABLE(
result_id integer,
url text,
title text,
description text,
tags text,
genre_name text,
genre_url text,
type text,
total_plays text,
rank real,
total_count bigint
)
AS $$
BEGIN
RETURN QUERY
SELECT
s.id::integer AS result_id,
s.url,
s.title,
s.description,
array_to_string(s.tags, ', ') AS tags,
s.genre_name,
s.genre_url,
s.type,
s.total_plays,
ts_rank_cd(to_tsvector('english', s.title || ' ' || s.description || array_to_string(s.tags, ' ')), plainto_tsquery('english', search_query)) AS rank,
COUNT(*) OVER() AS total_count
FROM
optimized_search_index s
WHERE(types IS NULL
OR s.type = ANY(types))
AND plainto_tsquery('english', search_query) @@ to_tsvector('english', s.title || ' ' || s.description || array_to_string(s.tags, ' '))
ORDER BY
s.type = 'post' DESC,
s.content_date DESC NULLS LAST,
rank DESC
LIMIT page_size OFFSET page_offset;
END;
$$
LANGUAGE plpgsql;

View file

@ -0,0 +1,26 @@
CREATE OR REPLACE VIEW optimized_links AS
SELECT
l.id,
l.title,
l.date,
l.description,
l.link,
a.mastodon,
a.name,
json_build_object('name', a.name, 'url', a.url, 'mastodon', a.mastodon) AS author,
'link' AS type,
(
SELECT
array_agg(t.name)
FROM
links_tags lt
LEFT JOIN tags t ON lt.tags_id = t.id
WHERE
lt.links_id = l.id) AS tags,
json_build_object('title', CONCAT(l.title, ' via ', a.name), 'url', l.link, 'description', l.description, 'date', l.date) AS feed
FROM
links l
JOIN authors a ON l.author = a.id
ORDER BY
l.date DESC;

View file

@ -0,0 +1,126 @@
CREATE OR REPLACE VIEW optimized_posts AS
SELECT
p.id,
p.date,
p.title,
p.description,
p.content,
p.featured,
p.slug AS url,
p.mastodon_url,
CASE WHEN df.filename_disk IS NOT NULL
AND df.filename_disk != ''
AND df.filename_disk != '/' THEN
CONCAT('/', df.filename_disk)
ELSE
NULL
END AS image,
p.image_alt,
CASE WHEN EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date)) > 3 THEN
TRUE
ELSE
FALSE
END AS old_post,
(
SELECT
json_agg(
CASE WHEN pb.collection = 'youtube_player' THEN
json_build_object('type', pb.collection, 'url', yp.url)
WHEN pb.collection = 'github_banner' THEN
json_build_object('type', pb.collection, 'url', gb.url)
WHEN pb.collection = 'npm_banner' THEN
json_build_object('type', pb.collection, 'url', nb.url, 'command', nb.command)
WHEN pb.collection = 'rss_banner' THEN
json_build_object('type', pb.collection, 'url', rb.url, 'text', rb.text)
WHEN pb.collection = 'hero' THEN
json_build_object('type', pb.collection, 'image', CONCAT('/', df_hero.filename_disk), 'alt_text', h.alt_text)
WHEN pb.collection = 'markdown' THEN
json_build_object('type', pb.collection, 'text', md.text)
WHEN pb.collection = 'divider' THEN
json_build_object('type', pb.collection, 'markup', d.markup)
ELSE
json_build_object('type', pb.collection)
END)
FROM
posts_blocks pb
LEFT JOIN youtube_player yp ON pb.collection = 'youtube_player'
AND yp.id = pb.item::integer
LEFT JOIN github_banner gb ON pb.collection = 'github_banner'
AND gb.id = pb.item::integer
LEFT JOIN npm_banner nb ON pb.collection = 'npm_banner'
AND nb.id = pb.item::integer
LEFT JOIN rss_banner rb ON pb.collection = 'rss_banner'
AND rb.id = pb.item::integer
LEFT JOIN hero h ON pb.collection = 'hero'
AND h.id = pb.item::integer
LEFT JOIN directus_files df_hero ON h.image = df_hero.id
LEFT JOIN markdown md ON pb.collection = 'markdown'
AND md.id = pb.item::integer
LEFT JOIN divider d ON pb.collection = 'divider'
AND d.id = pb.item::integer
WHERE
pb.posts_id = p.id) AS blocks,
(
SELECT
array_agg(t.name)
FROM
posts_tags pt
LEFT JOIN tags t ON pt.tags_id = t.id
WHERE
pt.posts_id = p.id) AS tags,
(
SELECT
json_agg(json_build_object('name', g.name, 'url', g.slug))
FROM
posts_genres gp
LEFT JOIN genres g ON gp.genres_id = g.id
WHERE
gp.posts_id = p.id) AS genres,
(
SELECT
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays))
FROM
posts_artists pa
LEFT JOIN artists a ON pa.artists_id = a.id
WHERE
pa.posts_id = p.id) AS artists,
(
SELECT
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
ORDER BY b.title)
FROM
posts_books pbk
LEFT JOIN books b ON pbk.books_id = b.id
WHERE
pbk.posts_id = p.id) AS books,
(
SELECT
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
ORDER BY m.year DESC)
FROM
posts_movies pm
LEFT JOIN movies m ON pm.movies_id = m.id
WHERE
pm.posts_id = p.id) AS movies,
(
SELECT
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug))
FROM
posts_shows ps
LEFT JOIN shows s ON ps.shows_id = s.id
WHERE
ps.posts_id = p.id) AS shows,
json_build_object('title', p.title, 'url', CONCAT('https://coryd.dev', p.slug), 'description', p.description, 'content', p.content, 'date', p.date, 'image', CASE WHEN df.filename_disk IS NOT NULL
AND df.filename_disk != ''
AND df.filename_disk != '/' THEN
CONCAT('/', df.filename_disk)
ELSE
NULL
END) AS feed
FROM
posts p
LEFT JOIN directus_files df ON p.image = df.id
GROUP BY
p.id,
df.filename_disk;

View file

@ -0,0 +1,79 @@
CREATE OR REPLACE VIEW optimized_all_activity AS
WITH feed_data AS (
SELECT
p.date AS content_date,
p.title,
p.content AS description,
CONCAT('https://coryd.dev', p.url) AS url,
NULL AS image,
NULL AS rating,
p.tags,
json_build_object('title', p.title, 'url', CONCAT('https://coryd.dev', p.url), 'description', p.content, 'date', p.date) AS feed
FROM
optimized_posts p
UNION ALL
SELECT
l.date AS content_date,
l.title,
l.description,
l.link AS url,
NULL AS image,
NULL AS rating,
l.tags,
json_build_object('title', CONCAT(l.title, ' via ', l.name), 'url', l.link, 'description', l.description, 'date', l.date) AS feed
FROM
optimized_links l
UNION ALL
SELECT
b.date_finished AS content_date,
b.title,
b.description,
CONCAT('https://coryd.dev', b.url) AS url,
b.image,
b.rating,
b.tags,
CASE WHEN LOWER(b.status) = 'finished' THEN
json_build_object('title', b.title, 'url', CONCAT('https://coryd.dev', b.url), 'description', CASE WHEN b.review IS NOT NULL THEN
b.review
ELSE
b.description
END, 'image', b.image, 'rating', b.rating, 'date', b.date_finished)
ELSE
NULL
END AS feed
FROM
optimized_books b
UNION ALL
SELECT
m.last_watched AS content_date,
m.title,
m.description,
CONCAT('https://coryd.dev', m.url) AS url,
m.image,
m.rating,
m.tags,
CASE WHEN m.last_watched IS NOT NULL THEN
json_build_object('title', m.title, 'url', CONCAT('https://coryd.dev', m.url), 'description', CASE WHEN m.review IS NOT NULL THEN
m.review
ELSE
m.description
END, 'image', m.backdrop, 'rating', m.rating, 'date', m.last_watched)
ELSE
NULL
END AS feed
FROM
optimized_movies m
)
SELECT
json_agg(feed_data.* ORDER BY feed_data.content_date DESC) AS feed
FROM (
SELECT
*
FROM
feed_data
WHERE
feed IS NOT NULL
ORDER BY
content_date DESC
LIMIT 20) AS feed_data;

View file

@ -0,0 +1,109 @@
CREATE OR REPLACE VIEW optimized_search_index AS
WITH search_data AS (
SELECT
'post' AS type,
CONCAT('📝 ', p.title) AS title,
CONCAT('https://coryd.dev', p.url) AS url,
p.description AS description,
p.tags,
NULL AS genre_name,
NULL AS genre_url,
NULL::text AS total_plays,
p.date AS content_date
FROM
optimized_posts p
UNION ALL
SELECT
'link' AS type,
CONCAT('🔗 ', l.title, ' via ', l.name) AS title,
l.link AS url,
l.description AS description,
l.tags,
NULL AS genre_name,
NULL AS genre_url,
NULL::text AS total_plays,
l.date AS content_date
FROM
optimized_links l
UNION ALL
SELECT
'book' AS type,
CASE WHEN b.rating IS NOT NULL THEN
CONCAT('📖 ', b.title, ' (', b.rating, ')')
ELSE
CONCAT('📖 ', b.title)
END AS title,
CONCAT('https://coryd.dev', b.url) AS url,
b.description AS description,
b.tags,
NULL AS genre_name,
NULL AS genre_url,
NULL::text AS total_plays,
b.date_finished AS content_date
FROM
optimized_books b
WHERE
LOWER(b.status) = 'finished'
UNION ALL
SELECT
'artist' AS type,
CONCAT(COALESCE(ar.emoji, ar.genre_emoji, '🎧'), ' ', ar.name) AS title,
CONCAT('https://coryd.dev', ar.url) AS url,
ar.description AS description,
ARRAY[ar.genre_name] AS tags,
ar.genre_name,
CONCAT('https://coryd.dev', ar.genre_slug) AS genre_url,
to_char(ar.total_plays::numeric, 'FM999,999,999,999') AS total_plays,
NULL AS content_date
FROM
optimized_artists ar
UNION ALL
SELECT
'genre' AS type,
CONCAT(COALESCE(g.emoji, '🎵'), ' ', g.name) AS title,
CONCAT('https://coryd.dev', g.url) AS url,
g.description AS description,
NULL AS tags,
g.name AS genre_name,
CONCAT('https://coryd.dev', g.url) AS genre_url,
NULL::text AS total_plays,
NULL AS content_date
FROM
optimized_genres g
UNION ALL
SELECT
'show' AS type,
CONCAT('📺 ', s.title, ' (', s.year, ')') AS title,
CONCAT('https://coryd.dev', s.url) AS url,
s.description AS description,
s.tags,
NULL AS genre_name,
NULL AS genre_url,
NULL::text AS total_plays,
s.last_watched_at AS content_date
FROM
optimized_shows s
WHERE
s.last_watched_at IS NOT NULL
UNION ALL
SELECT
'movie' AS type,
CONCAT('🎬 ', m.title, ' (', m.rating, ')') AS title,
CONCAT('https://coryd.dev', m.url) AS url,
m.description AS description,
m.tags,
NULL AS genre_name,
NULL AS genre_url,
NULL::text AS total_plays,
m.last_watched AS content_date
FROM
optimized_movies m
WHERE
m.rating IS NOT NULL
)
SELECT
ROW_NUMBER() OVER (ORDER BY url) AS id,
*
FROM
search_data;

View file

@ -0,0 +1,65 @@
CREATE OR REPLACE VIEW optimized_sitemap AS
WITH sitemap_data AS (
SELECT
p.date AS content_date,
p.title,
CONCAT('https://coryd.dev', p.url) AS url,
'monthly' AS changefreq,
0.7 AS priority
FROM
optimized_posts p
UNION ALL
SELECT
b.date_finished AS content_date,
b.title,
CONCAT('https://coryd.dev', b.url) AS url,
'monthly' AS changefreq,
0.5 AS priority
FROM
optimized_books b
UNION ALL
SELECT
m.last_watched AS content_date,
m.title,
CONCAT('https://coryd.dev', m.url) AS url,
'weekly' AS changefreq,
0.6 AS priority
FROM
optimized_movies m
UNION ALL
SELECT
NULL AS content_date,
ar.name AS title,
CONCAT('https://coryd.dev', ar.url) AS url,
'monthly' AS changefreq,
0.5 AS priority
FROM
optimized_artists ar
UNION ALL
SELECT
NULL AS content_date,
g.name AS title,
CONCAT('https://coryd.dev', g.url) AS url,
'yearly' AS changefreq,
0.3 AS priority
FROM
optimized_genres g
UNION ALL
SELECT
s.last_watched_at AS content_date,
s.title,
CONCAT('https://coryd.dev', s.url) AS url,
'weekly' AS changefreq,
0.8 AS priority
FROM
optimized_shows s
)
SELECT
url,
title,
content_date AS lastmod,
changefreq,
priority
FROM
sitemap_data;

View file

@ -0,0 +1,86 @@
CREATE OR REPLACE VIEW optimized_syndication AS
WITH syndication_data AS (
SELECT
p.date AS content_date,
p.title,
p.description,
CONCAT('https://coryd.dev', p.url) AS url,
p.tags,
json_build_object('title', CONCAT('📝 ', p.title, ' ',(
SELECT
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
FROM unnest(p.tags) AS t(name))), 'description', p.description, 'url', CONCAT('https://coryd.dev', p.url), 'date', p.date) AS syndication
FROM
optimized_posts p
UNION ALL
SELECT
l.date AS content_date,
l.title,
l.description,
l.link AS url,
l.tags,
json_build_object('title', CONCAT('🔗 ', l.title, CASE WHEN l.mastodon IS NOT NULL THEN
' via @' || split_part(l.mastodon, '@', 2) || '@' || split_part(split_part(l.mastodon, 'https://', 2), '/', 1)
ELSE
CONCAT(' via ', l.name)
END, ' ',(
SELECT
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
FROM unnest(l.tags) AS t(name))), 'description', l.description, 'url', l.link, 'date', l.date) AS syndication
FROM
optimized_links l
UNION ALL
SELECT
b.date_finished AS content_date,
b.title,
b.description,
CONCAT('https://coryd.dev', b.url) AS url,
b.tags,
CASE WHEN LOWER(b.status) = 'finished' THEN
json_build_object('title', CONCAT('📖 ', b.title, CASE WHEN b.rating IS NOT NULL THEN
' (' || b.rating || ')'
ELSE
''
END, ' ',(
SELECT
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
FROM unnest(b.tags) AS t(name))), 'description', b.description, 'url', CONCAT('https://coryd.dev', b.url), 'date', b.date_finished)
ELSE
NULL
END AS syndication
FROM
optimized_books b
UNION ALL
SELECT
m.last_watched AS content_date,
m.title,
m.description,
CONCAT('https://coryd.dev', m.url) AS url,
m.tags,
CASE WHEN m.last_watched IS NOT NULL THEN
json_build_object('title', CONCAT('🎥 ', m.title, CASE WHEN m.rating IS NOT NULL THEN
' (' || m.rating || ')'
ELSE
''
END, ' ',(
SELECT
array_to_string(array_agg('#' || initcap(replace(t.name, ' ', ''))), ' ')
FROM unnest(m.tags) AS t(name))), 'description', m.description, 'url', CONCAT('https://coryd.dev', m.url), 'date', m.last_watched)
ELSE
NULL
END AS syndication
FROM
optimized_movies m
)
SELECT
json_agg(limited_data.*) AS syndication
FROM (
SELECT
*
FROM
syndication_data
WHERE
syndication IS NOT NULL
ORDER BY
content_date DESC
LIMIT 20) AS limited_data;

View file

@ -0,0 +1,22 @@
CREATE OR REPLACE VIEW optimized_globals AS
SELECT
g.site_name,
g.site_description,
g.intro,
g.author,
g.email,
g.mastodon,
g.url,
g.cdn_url,
g.theme_color,
g.site_type,
g.locale,
g.lang,
g.webfinger_username,
g.webfinger_hostname,
CONCAT('/', df.filename_disk) AS avatar,
CONCAT('/', df2.filename_disk) AS avatar_transparent
FROM
globals g
LEFT JOIN directus_files df ON g.avatar = df.id
LEFT JOIN directus_files df2 ON g.avatar_transparent = df2.id

View file

@ -0,0 +1,14 @@
CREATE OR REPLACE VIEW optimized_navigation AS
SELECT
n.id,
n.menu_location,
n.permalink,
n.icon,
n.title,
n.sort,
p.title AS page_title,
p.permalink AS page_permalink
FROM
navigation n
LEFT JOIN pages p ON n.pages = p.id;

View file

@ -0,0 +1,56 @@
CREATE OR REPLACE VIEW optimized_pages AS
SELECT
p.id,
p.title,
p.permalink,
p.description,
CONCAT('/', df.filename_disk) AS open_graph_image,
p.updated,
(
SELECT
json_agg(
CASE WHEN pb.collection = 'youtube_player' THEN
json_build_object('type', pb.collection, 'url', yp.url)
WHEN pb.collection = 'github_banner' THEN
json_build_object('type', pb.collection, 'url', gb.url)
WHEN pb.collection = 'npm_banner' THEN
json_build_object('type', pb.collection, 'url', nb.url, 'command', nb.command)
WHEN pb.collection = 'rss_banner' THEN
json_build_object('type', pb.collection, 'url', rb.url, 'text', rb.text)
WHEN pb.collection = 'hero' THEN
json_build_object('type', pb.collection, 'image', CONCAT('/', df_hero.filename_disk), 'alt', h.alt_text)
WHEN pb.collection = 'markdown' THEN
json_build_object('type', pb.collection, 'text', md.text)
WHEN pb.collection = 'divider' THEN
json_build_object('type', pb.collection, 'markup', d.markup)
WHEN pb.collection = 'addon_links' THEN
json_build_object('type', pb.collection, 'addon_links', d.title)
ELSE
json_build_object('type', pb.collection)
END ORDER BY pb.sort)
FROM
pages_blocks pb
LEFT JOIN youtube_player yp ON pb.collection = 'youtube_player'
AND yp.id = pb.item::integer
LEFT JOIN github_banner gb ON pb.collection = 'github_banner'
AND gb.id = pb.item::integer
LEFT JOIN npm_banner nb ON pb.collection = 'npm_banner'
AND nb.id = pb.item::integer
LEFT JOIN rss_banner rb ON pb.collection = 'rss_banner'
AND rb.id = pb.item::integer
LEFT JOIN hero h ON pb.collection = 'hero'
AND h.id = pb.item::integer
LEFT JOIN directus_files df_hero ON h.image = df_hero.id
LEFT JOIN markdown md ON pb.collection = 'markdown'
AND md.id = pb.item::integer
LEFT JOIN divider d ON pb.collection = 'divider'
AND d.id = pb.item::integer
WHERE
pb.pages_id = p.id) AS blocks
FROM
pages p
LEFT JOIN directus_files df ON p.open_graph_image = df.id
GROUP BY
p.id,
df.filename_disk;

View file

@ -0,0 +1,95 @@
CREATE OR REPLACE VIEW optimized_books AS
SELECT
b.date_finished,
EXTRACT(YEAR FROM b.date_finished) AS year,
b.author,
b.description,
b.title,
b.progress,
b.read_status AS status,
b.star_rating AS rating,
b.review,
b.slug AS url,
CONCAT('/', df.filename_disk) AS image,
b.favorite,
b.tattoo,
(
SELECT
array_agg(t.name)
FROM
books_tags bt
LEFT JOIN tags t ON bt.tags_id = t.id
WHERE
bt.books_id = b.id) AS tags,
(
SELECT
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays))
FROM
books_artists ba
LEFT JOIN artists a ON ba.artists_id = a.id
WHERE
ba.books_id = b.id) AS artists,
(
SELECT
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug))
FROM
movies_books mb
LEFT JOIN movies m ON mb.movies_id = m.id
WHERE
mb.books_id = b.id) AS movies,
(
SELECT
json_agg(json_build_object('name', g.name, 'url', g.slug))
FROM
genres_books gb
LEFT JOIN genres g ON gb.genres_id = g.id
WHERE
gb.books_id = b.id) AS genres,
(
SELECT
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug))
FROM
shows_books sb
LEFT JOIN shows s ON sb.shows_id = s.id
WHERE
sb.books_id = b.id) AS shows,
(
SELECT
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
ORDER BY p.date DESC)
FROM
posts_books pb
LEFT JOIN posts p ON pb.posts_id = p.id
WHERE
pb.books_id = b.id) AS posts,
(
SELECT
json_agg(json_build_object('title', rb.title, 'author', rb.author, 'url', rb.slug)
ORDER BY rb.title)
FROM
related_books rbk
LEFT JOIN books rb ON rbk.related_books_id = rb.id
WHERE
rbk.books_id = b.id) AS related_books,
json_build_object('title', CONCAT(b.title, ' by ', b.author), 'image', CONCAT('/', df.filename_disk), 'url', b.slug, 'alt', CONCAT('Book cover from ', b.title, ' by ', b.author), 'subtext', CASE WHEN b.star_rating IS NOT NULL THEN
b.star_rating
ELSE
NULL
END) AS grid,
CASE WHEN LOWER(b.read_status) = 'finished'
AND b.star_rating IS NOT NULL THEN
json_build_object('title', b.title, 'url', CONCAT('https://coryd.dev', b.slug), 'date', b.date_finished, 'description', CASE WHEN b.review IS NOT NULL THEN
b.review
ELSE
b.description
END, 'image', CONCAT('/', df.filename_disk), 'rating', b.star_rating)
ELSE
NULL
END AS feed
FROM
books b
LEFT JOIN directus_files df ON b.art = df.id
GROUP BY
b.id,
df.filename_disk;

View file

@ -0,0 +1,106 @@
CREATE OR REPLACE VIEW optimized_movies AS
SELECT
m.id,
m.last_watched,
m.title,
m.year,
m.collected,
m.plays,
m.favorite,
m.tattoo,
m.star_rating AS rating,
m.description,
m.review,
m.slug AS url,
CONCAT('/', df.filename_disk) AS image,
CONCAT('/', df2.filename_disk) AS backdrop,
json_build_object('title', m.title, 'url', m.slug, 'image', CONCAT('/', df.filename_disk), 'backdrop', CONCAT('/', df2.filename_disk), 'alt', CONCAT('Poster from ', m.title, ' (', m.year, ')'), 'subtext', CASE WHEN m.star_rating IS NOT NULL THEN
CONCAT(m.star_rating, ' (', m.year, ')')
ELSE
CONCAT('(', m.year, ')')
END) AS grid,
(
SELECT
array_agg(t.name)
FROM
movies_tags mt
LEFT JOIN tags t ON mt.tags_id = t.id
WHERE
mt.movies_id = m.id) AS tags,
(
SELECT
json_agg(json_build_object('name', g.name, 'url', g.slug)
ORDER BY g.name ASC)
FROM
genres_movies gm
LEFT JOIN genres g ON gm.genres_id = g.id
WHERE
gm.movies_id = m.id) AS genres,
(
SELECT
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays)
ORDER BY a.name_string ASC)
FROM
movies_artists ma
LEFT JOIN artists a ON ma.artists_id = a.id
WHERE
ma.movies_id = m.id) AS artists,
(
SELECT
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
ORDER BY b.title ASC)
FROM
movies_books mb
LEFT JOIN books b ON mb.books_id = b.id
WHERE
mb.movies_id = m.id) AS books,
(
SELECT
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug)
ORDER BY s.year DESC)
FROM
shows_movies sm
LEFT JOIN shows s ON sm.shows_id = s.id
WHERE
sm.movies_id = m.id) AS shows,
(
SELECT
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
ORDER BY p.date DESC)
FROM
posts_movies pm
LEFT JOIN posts p ON pm.posts_id = p.id
WHERE
pm.movies_id = m.id) AS posts,
(
SELECT
json_agg(json_build_object('title', rm.title, 'year', rm.year, 'url', rm.slug)
ORDER BY rm.year DESC)
FROM
related_movies r
LEFT JOIN movies rm ON r.related_movies_id = rm.id
WHERE
r.movies_id = m.id) AS related_movies,
CASE WHEN m.star_rating IS NOT NULL
AND m.last_watched IS NOT NULL THEN
json_build_object('title', m.title, 'url', CONCAT('https://coryd.dev', m.slug), 'date', m.last_watched, 'description', CASE WHEN m.review IS NOT NULL THEN
m.review
ELSE
m.description
END, 'image', CONCAT('/', df2.filename_disk), 'rating', m.star_rating)
ELSE
NULL
END AS feed
FROM
movies m
LEFT JOIN directus_files df ON m.art = df.id
LEFT JOIN directus_files df2 ON m.backdrop = df2.id
GROUP BY
m.id,
df.filename_disk,
df2.filename_disk
ORDER BY
m.last_watched DESC,
m.favorite DESC,
m.title ASC;

View file

@ -0,0 +1,17 @@
CREATE OR REPLACE VIEW optimized_album_releases AS
SELECT
a.name AS title,
a.release_date,
a.release_link AS url,
a.total_plays,
CONCAT('/', df.filename_disk) AS image,
json_build_object('name', ar.name_string, 'url', ar.slug, 'description', ar.description) AS artist,
EXTRACT(EPOCH FROM a.release_date) AS release_timestamp,
json_build_object('title', a.name, 'image', CONCAT('/', df.filename_disk), 'url', a.release_link, 'alt', CONCAT(a.name, ' by ', ar.name_string), 'subtext', CONCAT(ar.name_string, ' / ', TO_CHAR(a.release_date, 'Mon FMDD, YYYY'))) AS grid
FROM
albums a
LEFT JOIN directus_files df ON a.art = df.id
LEFT JOIN artists ar ON a.artist = ar.id
WHERE
a.release_date IS NOT NULL;

View file

@ -0,0 +1,94 @@
CREATE OR REPLACE VIEW optimized_artists AS
SELECT
ar.name_string AS name,
ar.slug AS url,
ar.tentative,
to_char(ar.total_plays, 'FM999,999,999,999') AS total_plays, -- Format total_plays with commas
ar.country,
ar.description,
ar.favorite,
g.name AS genre_name,
g.slug AS genre_slug,
g.emoji AS genre_emoji,
json_build_object('name', g.name, 'url', g.slug, 'emoji', g.emoji) AS genre,
ar.emoji,
ar.tattoo,
CONCAT('/', df.filename_disk) AS image,
json_build_object('alt', CONCAT(to_char(ar.total_plays, 'FM999,999,999,999'), ' plays of ', ar.name_string), -- Format total_plays in alt text
'subtext', CONCAT(to_char(ar.total_plays, 'FM999,999,999,999'), ' plays') -- Format total_plays in subtext
) AS grid,
(
SELECT
json_agg(json_build_object('name', a.name, 'release_year', a.release_year, 'total_plays', to_char(a.total_plays, 'FM999,999,999,999'), -- Format total_plays here as well
'art', df_album.filename_disk)
ORDER BY a.release_year)
FROM
albums a
LEFT JOIN directus_files df_album ON a.art = df_album.id
WHERE
a.artist = ar.id) AS albums,
(
SELECT
json_agg(json_build_object('id', c.id, 'date', c.date, 'venue_name', v.name, 'venue_name_short', trim(split_part(v.name, ',', 1)), 'venue_latitude', v.latitude, 'venue_longitude', v.longitude, 'notes', c.notes)
ORDER BY c.date DESC)
FROM
concerts c
LEFT JOIN venues v ON c.venue = v.id
WHERE
c.artist = ar.id) AS concerts,
(
SELECT
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
ORDER BY b.date_finished DESC)
FROM
books_artists ba
LEFT JOIN books b ON ba.books_id = b.id
WHERE
ba.artists_id = ar.id) AS books,
(
SELECT
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
ORDER BY m.year DESC)
FROM
movies_artists ma
LEFT JOIN movies m ON ma.movies_id = m.id
WHERE
ma.artists_id = ar.id) AS movies,
(
SELECT
json_agg(json_build_object('title', s.title, 'year', s.year, 'url', s.slug)
ORDER BY s.year DESC)
FROM
shows_artists sa
LEFT JOIN shows s ON sa.shows_id = s.id
WHERE
sa.artists_id = ar.id) AS shows,
(
SELECT
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
ORDER BY p.date DESC)
FROM
posts_artists pa
LEFT JOIN posts p ON pa.posts_id = p.id
WHERE
pa.artists_id = ar.id) AS posts,
(
SELECT
json_agg(json_build_object('name', related_ar.name_string, 'url', related_ar.slug, 'country', related_ar.country, 'total_plays', to_char(related_ar.total_plays, 'FM999,999,999,999'))
ORDER BY related_ar.name_string)
FROM
related_artists ra
LEFT JOIN artists related_ar ON ra.related_artists_id = related_ar.id
WHERE
ra.artists_id = ar.id) AS related_artists
FROM
artists ar
LEFT JOIN directus_files df ON ar.art = df.id
LEFT JOIN genres g ON ar.genres = g.id
GROUP BY
ar.id,
df.filename_disk,
g.name,
g.slug,
g.emoji;

View file

@ -0,0 +1,19 @@
CREATE OR REPLACE VIEW optimized_concerts AS
SELECT
c.id,
c.date,
c.notes,
CASE WHEN c.artist IS NOT NULL THEN
json_build_object('name', a.name_string, 'url', a.slug)
ELSE
json_build_object('name', c.artist_name_string, 'url', NULL)
END AS artist,
json_build_object('name', v.name, 'name_short', trim(split_part(v.name, ',', 1)), 'latitude', v.latitude, 'longitude', v.longitude, 'notes', v.notes) AS venue,
c.notes AS concert_notes
FROM
concerts c
LEFT JOIN artists a ON c.artist = a.id
LEFT JOIN venues v ON c.venue = v.id
ORDER BY
c.date DESC;

View file

@ -0,0 +1,49 @@
CREATE OR REPLACE VIEW optimized_genres AS
SELECT
g.id,
g.name,
g.description,
g.emoji,
to_char(g.total_plays, 'FM999,999,999,999') AS total_plays,
g.wiki_link,
g.slug AS url,
(
SELECT
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'image', CONCAT('/', df_artist.filename_disk), 'total_plays', to_char(a.total_plays, 'FM999,999,999,999'))
ORDER BY a.total_plays DESC)
FROM
artists a
LEFT JOIN directus_files df_artist ON a.art = df_artist.id
WHERE
a.genres = g.id) AS artists,
(
SELECT
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug))
FROM
books b
JOIN genres_books gb ON gb.books_id = b.id
WHERE
gb.genres_id = g.id) AS books,
(
SELECT
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
ORDER BY m.year DESC)
FROM
movies m
JOIN genres_movies gm ON gm.movies_id = m.id
WHERE
gm.genres_id = g.id) AS movies,
(
SELECT
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
ORDER BY p.date DESC)
FROM
posts_genres pg
LEFT JOIN posts p ON pg.posts_id = p.id
WHERE
pg.genres_id = g.id) AS posts
FROM
genres g
ORDER BY
g.id ASC;

View file

@ -0,0 +1,28 @@
CREATE OR REPLACE VIEW optimized_listens AS SELECT DISTINCT ON (l.id, l.listened_at, l.track_name, l.artist_name, l.album_name)
l.id,
l.listened_at,
l.track_name,
l.artist_name,
l.album_name,
l.album_key,
CONCAT('/', df_art.filename_disk) AS artist_art,
a.genres AS artist_genres,
g.name AS genre_name,
g.slug AS genre_url,
a.country AS artist_country,
a.slug AS artist_url,
CONCAT('/', df_album.filename_disk) AS album_art
FROM
listens l
LEFT JOIN artists a ON (l.artist_name = a.name_string)
LEFT JOIN albums al ON (l.album_key = al.key)
LEFT JOIN directus_files df_art ON (a.art = df_art.id)
LEFT JOIN directus_files df_album ON (al.art = df_album.id)
LEFT JOIN genres g ON (a.genres = g.id)
ORDER BY
l.id,
l.listened_at,
l.track_name,
l.artist_name,
l.album_name;

View file

@ -0,0 +1,20 @@
CREATE OR REPLACE VIEW month_albums AS
SELECT
ol.album_name,
ol.artist_name,
COUNT(*) AS plays,
ol.album_art,
ol.artist_url,
json_build_object('title', ol.album_name, 'image', ol.album_art, 'url', ol.artist_url, 'alt', CONCAT(ol.album_name, ' by ', ol.artist_name), 'subtext', ol.artist_name) AS grid
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
GROUP BY
ol.album_name,
ol.artist_name,
ol.album_art,
ol.artist_url
ORDER BY
plays DESC;

View file

@ -0,0 +1,19 @@
CREATE OR REPLACE VIEW month_artists AS
SELECT
ol.artist_name,
COUNT(*) AS plays,
ol.artist_art,
ol.artist_url,
ARRAY_AGG(DISTINCT ol.genre_name) AS genres,
json_build_object('title', ol.artist_name, 'image', ol.artist_art, 'url', ol.artist_url, 'alt', CONCAT(COUNT(*), ' plays of ', ol.artist_name), 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
GROUP BY
ol.artist_name,
ol.artist_art,
ol.artist_url
ORDER BY
plays DESC;

View file

@ -0,0 +1,16 @@
CREATE OR REPLACE VIEW month_genres AS
SELECT
ol.genre_name,
ol.genre_url,
COUNT(*) AS plays,
json_build_object('alt', ol.genre_name, 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
GROUP BY
ol.genre_name,
ol.genre_url
ORDER BY
plays DESC;

View file

@ -0,0 +1,37 @@
CREATE OR REPLACE VIEW month_tracks AS
WITH track_stats AS (
SELECT
ol.track_name,
ol.artist_name,
ol.album_name,
COUNT(*) AS plays,
MAX(ol.listened_at) AS last_listened,
ol.album_art,
ol.artist_url,
MAX(COUNT(*)) OVER () AS most_played
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '30 days'
GROUP BY
ol.track_name,
ol.artist_name,
ol.album_name,
ol.album_art,
ol.artist_url
)
SELECT
track_name,
artist_name,
album_name,
plays,
last_listened,
album_art,
artist_url,
json_build_object('title', track_name, 'artist', artist_name, 'url', artist_url, 'plays', plays, 'alt', CONCAT(track_name, ' by ', artist_name), 'subtext', CONCAT(album_name, ' (', plays, ' plays)'), 'percentage', ROUND((plays::decimal / most_played) * 100, 2)) AS chart
FROM
track_stats
ORDER BY
plays DESC,
last_listened DESC;

View file

@ -0,0 +1,23 @@
CREATE OR REPLACE VIEW recent_tracks AS
SELECT
ol.id,
ol.listened_at,
ol.track_name,
ol.artist_name,
ol.album_name,
ol.album_key,
ol.artist_art,
ol.artist_genres,
ol.genre_name,
ol.artist_country,
ol.album_art,
ol.artist_url,
ol.genre_url,
json_build_object('title', ol.track_name, 'subtext', ol.artist_name, 'alt', CONCAT(ol.track_name, ' by ', ol.artist_name), 'url', ol.artist_url, 'image', ol.album_art, 'played_at', ol.listened_at) AS chart
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
ORDER BY
TO_TIMESTAMP(ol.listened_at) DESC;

View file

@ -0,0 +1,20 @@
CREATE OR REPLACE VIEW week_albums AS
SELECT
ol.album_name,
ol.artist_name,
COUNT(*) AS plays,
ol.album_art,
ol.artist_url,
json_build_object('title', ol.album_name, 'image', ol.album_art, 'url', ol.artist_url, 'alt', CONCAT(ol.album_name, ' by ', ol.artist_name), 'subtext', ol.artist_name) AS grid
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
GROUP BY
ol.album_name,
ol.artist_name,
ol.album_art,
ol.artist_url
ORDER BY
plays DESC;

View file

@ -0,0 +1,19 @@
CREATE OR REPLACE VIEW week_artists AS
SELECT
ol.artist_name,
COUNT(*) AS plays,
ol.artist_art,
ol.artist_url,
ARRAY_AGG(DISTINCT ol.genre_name) AS genres,
json_build_object('title', ol.artist_name, 'image', ol.artist_art, 'url', ol.artist_url, 'alt', CONCAT(COUNT(*), ' plays of ', ol.artist_name), 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
GROUP BY
ol.artist_name,
ol.artist_art,
ol.artist_url
ORDER BY
plays DESC;

View file

@ -0,0 +1,16 @@
CREATE OR REPLACE VIEW week_genres AS
SELECT
ol.genre_name,
ol.genre_url,
COUNT(*) AS plays,
json_build_object('alt', ol.genre_name, 'subtext', CONCAT(COUNT(*), ' plays')) AS grid
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
GROUP BY
ol.genre_name,
ol.genre_url
ORDER BY
plays DESC;

View file

@ -0,0 +1,37 @@
CREATE OR REPLACE VIEW week_tracks AS
WITH track_stats AS (
SELECT
ol.track_name,
ol.artist_name,
ol.album_name,
COUNT(*) AS plays,
MAX(ol.listened_at) AS last_listened,
ol.album_art,
ol.artist_url,
MAX(COUNT(*)) OVER () AS most_played
FROM
optimized_listens ol
WHERE
TO_TIMESTAMP(ol.listened_at) >= NOW() - INTERVAL '7 days'
GROUP BY
ol.track_name,
ol.artist_name,
ol.album_name,
ol.album_art,
ol.artist_url
)
SELECT
track_name,
artist_name,
album_name,
plays,
last_listened,
album_art,
artist_url,
json_build_object('title', track_name, 'artist', artist_name, 'url', artist_url, 'plays', plays, 'alt', CONCAT(track_name, ' by ', artist_name), 'subtext', CONCAT(album_name, ' (', plays, ' plays)'), 'percentage', ROUND((plays::decimal / most_played) * 100, 2)) AS chart
FROM
track_stats
ORDER BY
plays DESC,
last_listened DESC;

View file

@ -0,0 +1,94 @@
CREATE OR REPLACE VIEW optimized_shows AS
SELECT
s.id,
s.title,
s.year,
s.collected,
s.favorite,
s.tattoo,
s.description,
s.review,
s.slug AS url,
CONCAT('/', df_art.filename_disk) AS image,
CONCAT('/', df_backdrop.filename_disk) AS backdrop,
json_build_object('title', s.title, 'image', CONCAT('/', df_art.filename_disk), 'backdrop', CONCAT('/', df_backdrop.filename_disk), 'url', s.slug, 'alt', CONCAT('Poster from ', s.title, ' (', s.year, ')'), 'subtext', COALESCE((
SELECT
CASE WHEN e1.last_watched_at >= NOW() - INTERVAL '90 days' THEN
CONCAT('S', e1.season_number, 'E', e1.episode_number)
ELSE
CONCAT('(', s.year::text, ')')
END FROM episodes e1
WHERE
e1.show = s.id ORDER BY e1.last_watched_at DESC, e1.season_number DESC, e1.episode_number DESC LIMIT 1), CONCAT('(', s.year::text, ')'))) AS grid,
json_build_object('title', s.title, 'year', s.year, 'url', s.slug, 'image', CONCAT('/', df_art.filename_disk), 'backdrop', CONCAT('/', df_backdrop.filename_disk), 'formatted_episode', COALESCE((
SELECT
CONCAT('S', e2.season_number, 'E', e2.episode_number)
FROM episodes e2
WHERE
e2.show = s.id ORDER BY e2.season_number DESC, e2.episode_number DESC LIMIT 1)), 'last_watched_at', MAX(e.last_watched_at)) AS episode,
(
SELECT
json_agg(json_build_object('title', m.title, 'year', m.year, 'url', m.slug)
ORDER BY m.year DESC)
FROM
shows_movies sm
LEFT JOIN movies m ON sm.movies_id = m.id
WHERE
sm.shows_id = s.id) AS movies,
(
SELECT
json_agg(json_build_object('title', b.title, 'author', b.author, 'url', b.slug)
ORDER BY b.title)
FROM
shows_books sb
LEFT JOIN books b ON sb.books_id = b.id
WHERE
sb.shows_id = s.id) AS books,
(
SELECT
json_agg(json_build_object('title', p.title, 'date', p.date, 'url', p.slug)
ORDER BY p.date DESC)
FROM
posts_shows ps
LEFT JOIN posts p ON ps.posts_id = p.id
WHERE
ps.shows_id = s.id) AS posts,
(
SELECT
array_agg(t.name)
FROM
shows_tags st
LEFT JOIN tags t ON st.tags_id = t.id
WHERE
st.shows_id = s.id) AS tags,
(
SELECT
json_agg(json_build_object('title', rs.title, 'year', rs.year, 'url', rs.slug)
ORDER BY rs.year DESC)
FROM
related_shows sr
LEFT JOIN shows rs ON sr.related_shows_id = rs.id
WHERE
sr.shows_id = s.id) AS related_shows,
(
SELECT
json_agg(json_build_object('name', a.name_string, 'url', a.slug, 'country', a.country, 'total_plays', a.total_plays)
ORDER BY a.name_string)
FROM
shows_artists sa
LEFT JOIN artists a ON sa.artists_id = a.id
WHERE
sa.shows_id = s.id) AS artists,
MAX(e.last_watched_at) AS last_watched_at
FROM
shows s
LEFT JOIN episodes e ON s.id = e.show
LEFT JOIN directus_files df_art ON s.art = df_art.id
LEFT JOIN directus_files df_backdrop ON s.backdrop = df_backdrop.id
GROUP BY
s.id,
df_art.filename_disk,
df_backdrop.filename_disk
ORDER BY
MAX(e.last_watched_at) DESC;

33
scripts/worker-build.mjs Normal file
View file

@ -0,0 +1,33 @@
import fs from 'fs/promises'
import dotenv from 'dotenv-flow'
dotenv.config()
const workerName = process.argv[2]
if (!workerName) {
console.error('Please specify a worker name.')
process.exit(1)
}
const templatePath = `workers/${workerName}/wrangler.template.toml`
const outputPath = `workers/${workerName}/wrangler.toml`
async function generateToml() {
try {
const template = await fs.readFile(templatePath, 'utf8')
const output = template
.replace(/\${CF_ACCOUNT_ID}/g, process.env.CF_ACCOUNT_ID)
.replace(/\${CF_ZONE_ID}/g, process.env.CF_ZONE_ID)
.replace(/\${RSS_TO_MASTODON_KV_NAMESPACE_ID}/g, process.env.RSS_TO_MASTODON_KV_NAMESPACE_ID)
await fs.writeFile(outputPath, output)
console.log(`Generated wrangler.toml for ${workerName}`)
} catch (error) {
console.error('Error generating wrangler.toml:', error)
process.exit(1)
}
}
generateToml()

View file

@ -0,0 +1,26 @@
---
import NavLink from "@components/nav/NavLink.astro";
import { fetchGlobalData } from "@utils/data/global/index.js";
const { nav } = await fetchGlobalData(Astro);
---
<footer>
<nav aria-label="Social icons" class="social">
{
nav.footer_icons.map((link) => (
<NavLink url={link.permalink} title={link.title} icon={link.icon} />
))
}
</nav>
<nav aria-label="Secondary site navigation" class="sub-pages">
{
nav.footer_text.map((link, index) => (
<>
<NavLink url={link.permalink} title={link.title} icon={link.icon} />
{index < nav.footer_text.length - 1 && <span>/</span>}
</>
))
}
</nav>
</footer>

View file

@ -0,0 +1,21 @@
---
import Menu from "@components/nav/Menu.astro";
const { siteName, url, nav } = Astro.props;
const isHomePage = url === "/";
---
<section class="main-title">
<h1>
{
isHomePage ? (
siteName
) : (
<a href="/" tabindex="0">
{siteName}
</a>
)
}
</h1>
<Menu nav={nav} />
</section>

View file

@ -0,0 +1,42 @@
---
import icons from "@cdransf/astro-tabler-icons";
const {
IconArticle,
IconHeadphones,
IconDeviceTvOld,
IconBooks,
IconLink,
IconInfoCircle,
IconSearch,
IconRss,
IconBrandMastodon,
IconMail,
IconBrandGithub,
IconBrandNpm,
IconCoffee,
IconDeviceWatch,
IconHeartHandshake,
} = icons;
const { icon, className } = Astro.props;
const iconComponents = {
article: IconArticle,
headphones: IconHeadphones,
"device-tv-old": IconDeviceTvOld,
books: IconBooks,
link: IconLink,
"info-circle": IconInfoCircle,
search: IconSearch,
rss: IconRss,
"brand-mastodon": IconBrandMastodon,
mail: IconMail,
"brand-github": IconBrandGithub,
"brand-npm": IconBrandNpm,
coffee: IconCoffee,
"device-watch": IconDeviceWatch,
"heart-handshake": IconHeartHandshake,
};
const SelectedIcon = iconComponents[icon?.toLowerCase()] || null;
---
{SelectedIcon ? <div set:html={SelectedIcon({ size: 24, className })} /> : null}

View file

@ -0,0 +1,105 @@
---
import { parseISO, format } from "date-fns";
import IconMapper from "@components/IconMapper.astro";
const {
artists = [],
books = [],
genres = [],
movies = [],
posts = [],
shows = [],
} = Astro.props;
const media = [
...(artists || []),
...(books || []),
...(genres || []),
...(movies || []),
...(posts || []),
...(shows || []),
];
if (media.length === 0) return null;
const sections = [
{
key: "artists",
icon: "headphones",
cssClass: "music",
label: "Related artist(s)",
items: artists || [],
},
{
key: "books",
icon: "books",
cssClass: "books",
label: "Related book(s)",
items: books || [],
},
{
key: "genres",
icon: "headphones",
cssClass: "music",
label: "Related genre(s)",
items: genres || [],
},
{
key: "movies",
icon: "movie",
cssClass: "movies",
label: "Related movie(s)",
items: movies || [],
},
{
key: "posts",
icon: "article",
cssClass: "article",
label: "Related post(s)",
items: posts || [],
},
{
key: "shows",
icon: "device-tv-old",
cssClass: "tv",
label: "Related show(s)",
items: shows || [],
},
];
---
<div class="associated-media">
{
sections.map(({ key, icon, cssClass, label, items }) => {
if (!items.length) return null;
return (
<section id={key} class={cssClass}>
<div class="media-title">
<IconMapper icon={icon} /> {label}
</div>
<ul>
{items.map((item) => (
<li>
<a href={item.url}>{item.title || item.name}</a>
{key === "artists" && item.total_plays > 0 && (
<strong class="highlight-text">
{item.total_plays}{" "}
{item.total_plays === 1 ? "play" : "plays"}
</strong>
)}
{key === "books" && <span>by {item.author}</span>}
{(key === "movies" || key === "shows") && (
<span>({item.year})</span>
)}
{key === "posts" && (
<span>({format(parseISO(item.date), "PPPP")})</span>
)}
</li>
))}
</ul>
</section>
);
})
}
</div>

View file

@ -0,0 +1,63 @@
---
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/general.js";
import { getPopularPosts } from "@utils/getPopularPosts.js";
const [analytics, links, posts] = await Promise.all([
fetchAnalyticsData(),
fetchLinks(),
fetchAllPosts(),
]);
const popularPosts = getPopularPosts(posts, analytics);
const { blocks } = Astro.props;
---
<div>
{
blocks.map((block) => (
<>
{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)} />}
{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)} />
)}
{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} />}
</>
))
}
</div>

View file

@ -0,0 +1,26 @@
---
import { fetchGlobalData } from "@utils/data/global/index.js";
const { image, alt } = Astro.props;
const { globals } = await fetchGlobalData(Astro);
---
<div class="hero">
<img
srcset={`
${globals.cdn_url}${image}?class=bannersm&type=webp 256w,
${globals.cdn_url}${image}?class=bannermd&type=webp 512w,
${globals.cdn_url}${image}?class=bannerbase&type=webp 1024w
`}
sizes="(max-width: 450px) 256px,
(max-width: 850px) 512px,
1024px"
src={`${globals.cdn_url}${image}?class=bannersm&type=webp`}
alt={alt}
class="image-banner"
loading="lazy"
decoding="async"
width="720"
height="480"
/>
</div>

View file

@ -0,0 +1,26 @@
---
import { md } from "@utils/helpers/general.js";
import icons from "@cdransf/astro-tabler-icons";
const { IconCircleX, IconInfoCircle } = icons;
const { content, id } = Astro.props;
---
<>
<input
class="modal-input"
id={id}
type="checkbox"
tabindex="0"
/>
<label class="modal-toggle" for={id}>
<div set:html={IconInfoCircle({ size: 24 })}/>
</label>
<div class="modal-wrapper">
<div class="modal-body">
<label class="modal-close" for={id}>
<div set:html={IconCircleX({ size: 24 })}/>
</label>
<div set:html={md(content)}/>
</div>
</div>
</>

View file

@ -0,0 +1,7 @@
---
import { YouTube } from 'astro-embed';
const { url } = Astro.props;
---
<YouTube id={url} />

View file

@ -0,0 +1,14 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconCoffee } = icons;
---
<div class="banner coffee">
<p>
<span set:html={IconCoffee({ size: 24 })} />
<a class="coffee" href="https://buymeacoffee.com/cory">
If you found this post helpful, you can buy me a coffee.
</a>
</p>
</div>

View file

@ -0,0 +1,10 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconAlertCircle } = icons;
const { text } = Astro.props;
---
<div class="banner error">
<p><span set:html={IconAlertCircle({ size: 24 })}/> {text}</p>
</div>

View file

@ -0,0 +1,14 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconBrandGithub } = icons;
const { url } = Astro.props;
---
<div class="banner github">
<p>
<span set:html={IconBrandGithub({ size: 24 })}/> Take a look at <a href={url}
>the GitHub repository for this project</a
>. (Give it a star if you feel like it.)
</p>
</div>

View file

@ -0,0 +1,13 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconBrandMastodon } = icons;
const { url } = Astro.props;
---
<div class="banner mastodon">
<p>
<span set:html={IconBrandMastodon({ size: 24 })} />
<a class="mastodon" href={url}> Discuss this post on Mastodon. </a>
</p>
</div>

View file

@ -0,0 +1,14 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconBrandNpm } = icons;
const { url, command } = Astro.props;
---
<div class="banner npm">
<p>
<span set:html={IconBrandNpm({ size: 24 })}/>
<a href={url}>You can take a look at this package on NPM</a> or install it by
running <code>{command}</code>.
</p>
</div>

View file

@ -0,0 +1,18 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconClockX } = icons;
const { isOldPost } = Astro.props;
---
{
isOldPost && (
<div class="banner old-post">
<p>
<span set:html={IconClockX({ size: 24 })}/>
This post is over 3 years old. I've probably changed my mind since it
was written and it <em>could</em> be out of date.
</p>
</div>
)
}

View file

@ -0,0 +1,13 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconRss } = icons;
const { url, text } = Astro.props;
---
<div class="banner rss">
<p>
<span set:html={IconRss({ size: 24 })}/>
<a href={url}>{text}</a>.
</p>
</div>

View file

@ -0,0 +1,13 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconAlertTriangle } = icons;
const { text } = Astro.props;
---
<div class="banner warning">
<p>
<span set:html={IconAlertTriangle({ size: 24 })}/>
{text}
</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,26 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconChartBarPopular } = icons;
const { popularPosts } = Astro.props;
---
{
popularPosts && popularPosts.length > 0 && (
<article>
<h3>
<a class="article" href="/posts">
<div set:html={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,34 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconLink } = icons;
const { links } = Astro.props;
---
{
links && links.length > 0 && (
<article>
<h3>
<a class="link" href="/links">
<div set:html={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

@ -0,0 +1,7 @@
---
const { intro } = Astro.props;
---
<article class="intro">
<div set:html={intro} />
</article>

View file

@ -0,0 +1,76 @@
---
import { fetchBooks } from "@utils/data/books.js";
import { fetchLinks } from "@utils/data/links.js";
import { fetchMovies } from "@utils/data/movies.js";
import { fetchMusicWeek } from "@utils/data/music/week.js";
import { fetchShows } from "@utils/data/tv.js";
import icons from "@cdransf/astro-tabler-icons";
import Rss from "@components/blocks/banners/Rss.astro";
const { IconActivity } = icons;
const [music, tv, movies, books, links] = await Promise.all([
fetchMusicWeek(),
fetchShows(),
fetchMovies(),
fetchBooks(),
fetchLinks(),
]);
const artist = music.week?.artists[0];
const track = music.week?.tracks[0];
const show = tv.recentlyWatched[0];
const movie = movies.recentlyWatched[0];
const book = books.currentYear[0];
const link = links[0];
---
<article>
<h2>
<div set:html={IconActivity({ size: 24 })}/>
Recent activity
</h2>
<ul>
<li>
<span class="music">Top artist this week:</span>
<a href={artist.artist_url}>{artist.artist_name}</a>
</li>
<li>
<span class="music">Top track this week:</span>
<a href={track.artist_url}>{track.track_name} by {track.artist_name}</a>
</li>
<li>
<span class="tv">Last episode watched:</span>
<strong class="highlight-text">{show.formatted_episode}</strong> of <a
href={show.url}>{show.title}</a
>
</li>
<li>
<span class="movies">Last movie watched:</span>
<a href={movie.url}>{movie.title}</a>{
movie.rating ? ` (${movie.rating})` : ""
}
</li>
<li>
<span class="books">Last book finished:</span>
<a href={book.url}>{book.title}</a> by {book.author}{
book.rating ? ` (${book.rating})` : ""
}
</li>
<li>
<span class="link">Last link shared:</span>
<a href={link.link}>{link.title}</a>
{
link.author && (
<span>
{" "}
via <a href={link.author.url}>{link.author.name}</a>
</span>
)
}
</li>
</ul>
<Rss
url="/feeds"
text="Subscribe to my movies, books, links or activity feed(s)"
/>
</article>

View file

@ -0,0 +1,36 @@
---
import icons from "@cdransf/astro-tabler-icons";
import { fetchAllPosts } from "@utils/data/posts.js";
import { md } from "@utils/helpers/general.js";
const { IconClock, IconStar, IconArrowRight } = icons;
const posts = await fetchAllPosts();
---
<h2>
<div set:html={IconClock({ size: 24 })}/>
Recent posts
</h2>
{
posts.slice(0, 5).map((post) => (
<article key={post.url}>
<div class="post-meta">
{post.featured && <div set:html={IconStar({ size: 16 })}/>}
<time datetime={post.date}>
{new Date(post.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
</div>
<h3>
<a href={post.url}>{post.title}</a>
</h3>
<p set:html={md(post.description)} />
</article>
))
}
<a class="icon-link" href="/posts">
View all posts <div set:html={IconArrowRight({ size: 16 })}/>
</a>

View file

@ -0,0 +1,75 @@
---
import Paginator from "@components/nav/Paginator.astro";
import { fetchGlobalData } from "@utils/data/global/index.js";
const { data, count, shape, pagination, loading = "lazy" } = Astro.props;
const { globals } = await fetchGlobalData(Astro);
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>
)}

View file

@ -0,0 +1,33 @@
---
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,48 @@
---
import { fetchGlobalData } from "@utils/data/global/index.js";
const { data } = Astro.props;
const { globals } = await fetchGlobalData(Astro);
---
<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 } = 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 image={movie.backdrop} alt={movie.title} />
</div>
</a>

View file

@ -0,0 +1,33 @@
---
import icons from "@cdransf/astro-tabler-icons";
import NavLink from "@components/nav/NavLink.astro";
const { IconMenu2, IconX } = icons
const { nav } = Astro.props;
---
<menu>
<input id="menu-toggle" type="checkbox" aria-hidden="true" />
<label class="menu-button-container" for="menu-toggle" tabindex="0">
<div class="menu-closed" aria-hidden="true">
<div set:html={IconMenu2({ size: 24 })}/>
</div>
<div class="menu-open" aria-hidden="true">
<div set:html={IconX({ size: 24 })}/>
</div>
</label>
<ul
class="menu-primary"
aria-label="Primary site navigation"
id="primary-navigation"
>
{
nav.primary.map((link) => (
<li>
<NavLink url={link.permalink} title={link.title} icon={link.icon} />
</li>
))
}
</ul>
</menu>

View file

@ -0,0 +1,27 @@
---
import IconMapper from "@components/IconMapper.astro";
import { removeTrailingSlash } from "@utils/helpers/general.js";
const { url, title, icon } = Astro.props;
const isHttp = url?.startsWith("http");
const isActive = Astro.url.pathname === removeTrailingSlash(url);
---
{
isActive ? (
<span class={`active icon ${icon?.toLowerCase()}`} aria-current="page">
<IconMapper icon={icon} />
<span>{title}</span>
</span>
) : (
<a
class={`icon ${icon}`}
href={url}
rel={isHttp ? "me" : undefined}
aria-label={title}
>
<IconMapper icon={icon} />
<span>{title}</span>
</a>
)
}

View file

@ -0,0 +1,50 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconArrowLeft, IconArrowRight } = icons;
const { pagination } = Astro.props;
const {
currentPage,
totalPages,
hasPrevious,
hasNext,
previousPage,
nextPage,
pages,
} = pagination;
---
<nav aria-label="Pagination" class="pagination">
<a
href={hasPrevious ? previousPage : "#"}
aria-label="Previous page"
class={hasPrevious ? "" : "disabled"}
>
<div set:html={IconArrowLeft({ size: 24 })}/>
</a>
<select class="client-side" aria-label="Page selection">
{pages.map((page, index) => (
<option
value={index}
data-href={page.href}
selected={page.number === currentPage}
>
{page.number} of {totalPages}
</option>
))}
</select>
<noscript>
<p>
<span aria-current="page">{currentPage}</span> of {totalPages}
</p>
</noscript>
<a
href={hasNext ? nextPage : "#"}
aria-label="Next page"
class={hasNext ? "" : "disabled"}
>
<div set:html={IconArrowRight({ size: 24 })}/>
</a>
</nav>

View file

@ -0,0 +1,8 @@
---
const { content } = Astro.props;
---
<div data-toggle-content class="text-toggle-hidden">
<div set:html={content} />
</div>
<button data-toggle-button>Show more</button>

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

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

120
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,120 @@
---
import "@styles/index.css";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import { fetchGlobalData } from "@utils/data/global/index.js";
import { md, htmlToText, htmlTruncate } from "@utils/helpers/general.js";
const { globals, nav } = await fetchGlobalData(Astro);
const currentUrl = Astro.url.pathname;
const isProduction = import.meta.env.MODE === "production";
const {
schema = "page",
pageTitle = globals.site_name,
description = globals.site_description,
ogImage = globals.avatar,
fullUrl = currentUrl,
} = Astro.props;
const pageDescription = md(description);
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>
{
pageTitle !== globals.site_name
? `${pageTitle} / ${globals.site_name}`
: pageTitle
}
</title>
<link
rel="preload"
href="/fonts/ml.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="/fonts/mlb.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link rel="canonical" href={`${globals.url}${fullUrl}`} />
<meta
property="og:title"
content={pageTitle !== globals.site_name
? `${pageTitle} / ${globals.site_name}`
: pageTitle}
/>
<meta
name="description"
content={htmlToText(htmlTruncate(pageDescription))}
/>
<meta
property="og:description"
content={htmlToText(htmlTruncate(pageDescription))}
/>
<meta property="og:type" content={schema || "page"} />
<meta property="og:url" content={fullUrl} />
<meta
property="og:image"
content={`${globals.cdn_url}${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={`${globals.url}/feeds/posts.xml`}
/>
<link rel="sitemap" href="/sitemap-index.xml" />
<script defer src="/scripts/index.js" is:inline></script>
{
isProduction && (
<script defer data-domain="coryd.dev" src="/js/script.js" />
)
}
<noscript>
<style>
.client-side {
display: none;
}
</style>
</noscript>
</head>
<body>
<div class="main-wrapper">
<main>
<Header siteName={globals.site_name} url={currentUrl} nav={nav} />
<div class="default-wrapper">
<slot />
</div>
</main>
<Footer />
</div>
</body>
</html>

108
src/middleware.js Normal file
View file

@ -0,0 +1,108 @@
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchNavigation } from "@utils/data/nav.js";
import { fetchArtistByUrl } from "@utils/data/dynamic/artistByUrl.js";
import { fetchBookByUrl } from "@utils/data/dynamic/bookByUrl.js";
import { fetchGenreByUrl } from "@utils/data/dynamic/genreByUrl.js";
import { fetchMovieByUrl } from "@utils/data/dynamic/movieByUrl.js";
import { fetchShowByUrl } from "@utils/data/dynamic/showByUrl.js";
import { isbnRegex } from "@utils/helpers/media.js";
import { isExcludedPath } from "@utils/helpers/general.js";
import { CACHE_DURATION } from "@utils/constants/index.js";
let cachedGlobals = null;
let cachedNav = null;
let cachedByType = {};
let lastFetchTimeGlobalsNav = 0;
let lastFetchTimeByType = {};
export async function onRequest(context, next) {
const now = Date.now();
const { request, locals } = context;
try {
const runtimeEnv = locals.runtime?.env;
if (!runtimeEnv)
return new Response("Internal Server Error", { status: 500 });
const urlPath = new URL(request.url).pathname;
if (
!cachedGlobals ||
!cachedNav ||
now - lastFetchTimeGlobalsNav > CACHE_DURATION
) {
const [globals, nav] = await Promise.all([
fetchGlobals(runtimeEnv),
fetchNavigation(runtimeEnv),
]);
cachedGlobals = globals;
cachedNav = nav;
lastFetchTimeGlobalsNav = now;
}
let resourceType = null;
if (urlPath.startsWith("/music/artists/")) resourceType = "artist";
else if (isbnRegex.test(urlPath)) resourceType = "book";
else if (urlPath.startsWith("/music/genres/")) resourceType = "genre";
else if (
urlPath.startsWith("/watching/movies/") &&
!isExcludedPath(urlPath, ["/favorites", "/recent"])
)
resourceType = "movie";
else if (
urlPath.startsWith("/watching/shows/") &&
!isExcludedPath(urlPath, ["/favorites", "/recent"])
)
resourceType = "show";
if (resourceType) {
if (
!cachedByType[urlPath] ||
now - (lastFetchTimeByType[urlPath] || 0) > CACHE_DURATION
) {
let fetchFunction = null;
switch (resourceType) {
case "artist":
fetchFunction = fetchArtistByUrl;
break;
case "book":
fetchFunction = fetchBookByUrl;
break;
case "genre":
fetchFunction = fetchGenreByUrl;
break;
case "movie":
fetchFunction = fetchMovieByUrl;
break;
case "show":
fetchFunction = fetchShowByUrl;
break;
}
const data = await fetchFunction(runtimeEnv, urlPath);
if (!data)
return new Response(
`${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} Not Found`,
{ status: 404 }
);
cachedByType[urlPath] = data;
lastFetchTimeByType[urlPath] = now;
}
locals[resourceType] = cachedByType[urlPath];
}
locals.globals = cachedGlobals;
locals.nav = cachedNav;
} catch (error) {
console.error("Error in middleware:", error);
return new Response("Internal Server Error", { status: 500 });
}
return next();
}

View file

@ -0,0 +1,48 @@
import { fetchGlobals } from "@utils/data/globals.js";
export const prerender = true;
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 });
}
}

60
src/pages/404.astro Normal file
View file

@ -0,0 +1,60 @@
---
import Layout from "@layouts/Layout.astro";
import AddonLinks from "@components/blocks/links/AddonLinks.astro";
import { getPopularPosts } from "@utils/getPopularPosts.js";
import { fetchAllPosts } from "@data/posts.js";
import { fetchAnalyticsData } from "@data/analytics.js";
import { fetchLinks } from "@data/links.js";
import { fetchGlobalData } from "@utils/data/global/index.js";
export const prerender = true;
const [analytics, links, posts] = await Promise.all([
fetchAnalyticsData(),
fetchLinks(),
fetchAllPosts(),
]);
const popularPosts = getPopularPosts(posts, analytics);
const { globals } = await fetchGlobalData(Astro);
const pageTitle = "404";
const description = "What kind of idiots do you have working here?";
---
<Layout
pageTitle={pageTitle}
description={description}
currentUrl={Astro.url.pathname}
>
<div class="hero">
<img
srcset="
https://cdn.coryd.dev/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannersm&type=webp 256w,
https://cdn.coryd.dev/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannermd&type=webp 512w,
https://cdn.coryd.dev/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannerbase&type=webp 1024w
"
srcset={`
${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannersm&type=webp 256w,
${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannermd&type=webp 512w,
${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannerbase&type=webp 1024w,
`}
sizes="(max-width: 450px) 256px,
(max-width: 850px) 512px,
1024px"
src={`${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannersm&type=webp`}
alt={description}
class="image-banner"
loading="lazy"
decoding="async"
width="720"
height="480"
/>
</div>
<div style="text-align:center">
<h2>{pageTitle}</h2>
<p>{description}</p>
<p><a href="/">Hurry up and skip out on the room service bill!</a></p>
</div>
<hr />
<AddonLinks popularPosts={popularPosts} links={links} />
</Layout>

View file

@ -0,0 +1,27 @@
---
import Layout from "@layouts/Layout.astro";
import BlockRenderer from "@components/blocks/BlockRenderer.astro";
import { fetchPages } from "@utils/data/pages.js";
export const prerender = true;
export async function getStaticPaths() {
const pages = await fetchPages();
return pages.map((page) => ({
params: { permalink: page.permalink },
props: { page },
}));
}
const { page } = Astro.props;
const currentUrl = Astro.url.pathname;
---
<Layout
pageTitle={page.title}
description={page.description}
ogImage={page.open_graph_image}
currentUrl={currentUrl}
>
<BlockRenderer blocks={page.blocks} />
</Layout>

97
src/pages/blogroll.astro Normal file
View file

@ -0,0 +1,97 @@
---
import Layout from "@layouts/Layout.astro";
import icons from "@cdransf/astro-tabler-icons";
import { fetchBlogroll } from "@utils/data/blogroll.js";
export const prerender = true;
const {
IconRss,
IconJson,
IconMailPlus,
IconBrandMastodon,
} = icons;
const blogroll = await fetchBlogroll();
const title = "Blogroll";
const description =
"These are awesome blogs that I enjoy and you may enjoy too.";
---
<Layout
pageTitle={title}
description={description}
currentUrl={Astro.url.pathname}
>
<h2 class="page-title">{title}</h2>
<p>
You can <a
href="/blogroll.opml"
>download an OPML file</a
> containing all of these feeds and import them into your RSS reader.
</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Link</th>
<th>Subscribe</th>
</tr>
</thead>
<tbody>
{
blogroll.map((blog) => (
<tr>
<td>{blog.name}</td>
<td>
<a href={blog.url}>{blog.url.replace("https://", "")}</a>
</td>
<td class="blog-roll-icons">
{blog.rss_feed && (
<a
class="rss"
href={blog.rss_feed}
aria-label={`RSS feed for ${blog.name}`}
>
<div set:html={IconRss({ size: 16 })}/>
</a>
)}
{blog.json_feed && (
<a
class="json"
href={blog.json_feed}
aria-label={`JSON feed for ${blog.name}`}
>
<div set:html={IconJson({ size: 16 })}/>
</a>
)}
{blog.newsletter && (
<a
class="mail-plus"
href={blog.newsletter}
aria-label={`Subscribe to ${blog.name}'s newsletter`}
>
<div set:html={IconMailPlus({ size: 16 })}/>
</a>
)}
{blog.mastodon && (
<a
class="brand-mastodon"
href={blog.mastodon}
aria-label={`Follow ${blog.name} on Mastodon`}
>
<div set:html={IconBrandMastodon({ size: 16 })}/>
</a>
)}
</td>
</tr>
))
}
</tbody>
</table>
<p>
Head on over to <a href="https://blogroll.org">blogroll.org</a> to find more
blogs to follow or search for feeds using <a href="https://feedle.world"
>feedle</a
>.
</p>
</Layout>

View file

@ -0,0 +1,46 @@
import { fetchBlogroll } from "@utils/data/blogroll.js";
import { fetchGlobals } from '@utils/data/globals.js';
export const prerender = true;
export async function GET() {
try {
const blogroll = await fetchBlogroll();
const globals = await fetchGlobals();
const dateCreated = new Date().toUTCString();
const opmlContent = `
<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
<title>OPML for all feeds in ${globals.site_name}'s blogroll</title>
<dateCreated>${dateCreated}</dateCreated>
</head>
<body>
${blogroll
.map(
(blog) => `
<outline
text="${blog.name}"
title="${blog.name}"
type="rss"
xmlUrl="${blog.rss_feed}"
htmlUrl="${blog.url}"
/>`
)
.join("\n")}
</body>
</opml>
`.trim();
return new Response(opmlContent, {
status: 200,
headers: {
"Content-Type": "application/xml",
},
});
} catch (error) {
console.error("Error generating blogroll OPML:", error);
return new Response("Error generating blogroll OPML", { status: 500 });
}
}

View file

@ -0,0 +1,114 @@
---
import Layout from "@layouts/Layout.astro";
import Warning from "@components/blocks/banners/Warning.astro";
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
import ProgressBar from "@components/media/ProgressBar.astro";
import icons from "@cdransf/astro-tabler-icons";
import { fetchGlobalData } from "@utils/data/global/index.js";
import { md } from "@utils/helpers/general.js";
const { IconArrowLeft, IconHeart, IconNeedle } = icons;
const { book, globals } = await fetchGlobalData(Astro, Astro.url.pathname);
if (!book) return Astro.redirect("/404", 404);
const alt = `${book.title}${book.author ? ` by ${book.author}` : ""}`;
const pageTitle = `${book.title} by ${book.author} / Books`;
const description =
book.description || `Details about ${book.title} by ${book.author}.`;
---
<Layout
pageTitle={pageTitle}
description={description}
fullUrl={Astro.url.pathname}
ogImage={book.image}
>
<a class="back-link" href="/books" title="Go back to the books index page">
<div set:html={IconArrowLeft({ size: 18 })}/> Back to books
</a>
<article class="book-focus">
<div class="book-display">
<img
srcset={`
${globals.cdn_url}${book.image}?class=verticalsm&type=webp 200w,
${globals.cdn_url}${book.image}?class=verticalmd&type=webp 400w,
${globals.cdn_url}${book.image}?class=verticalbase&type=webp 800w
`}
sizes="(max-width: 450px) 203px,
(max-width: 850px) 406px,
(max-width: 1000px) 812px,
812px"
src={`${globals.cdn_url}${book.image}?class=verticalsm&type=webp`}
alt={alt}
loading="lazy"
decoding="async"
width="200"
height="307"
/>
<div class="media-meta">
<span class="title"><strong>{book.title}</strong></span>
{book.rating && <span>{book.rating}</span>}
{book.author && <span class="sub-meta">By {book.author}</span>}
{
book.favorite && (
<span class="sub-meta favorite">
<div set:html={IconHeart({ size: 18 })}/> This is one of my favorite books!
</span>
)
}
{
book.tattoo && (
<span class="sub-meta tattoo">
<div set:html={IconNeedle({ size: 18 })}/> I have a tattoo inspired by this book!
</span>
)
}
{
book.status === "finished" && (
<span class="sub-meta">
Finished on:{" "}
<strong class="highlight-text">
{new Date(book.date_finished).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})}
</strong>
</span>
)
}
{
book.status === "started" && (
<ProgressBar percentage={`${book.progress}%`} />
)
}
</div>
</div>
{
book.review && (
<>
<Warning text="There are probably spoilers after this banner — this is a warning about them." />
<h2>My thoughts</h2>
<div set:html={md(book.review)} />
</>
)
}
<AssociatedMedia
artists={book.artists}
books={book.related_books}
genres={book.genres}
movies={book.movies}
posts={book.posts}
shows={book.shows}
/>
{
book.description && (
<>
<h2>Overview</h2>
<div set:html={md(book.description)} />
</>
)
}
</article>
</Layout>

View file

@ -0,0 +1,78 @@
---
import Layout from "@layouts/Layout.astro";
import Rss from "@components/blocks/banners/Rss.astro";
import ProgressBar from "@components/media/ProgressBar.astro";
import { fetchBooks } from "@utils/data/books.js";
import { fetchGlobalData } from "@utils/data/global/index.js";
import { md, htmlTruncate } from "@utils/helpers/general.js";
import { bookYearLinks } from "@utils/helpers/media.js";
export const prerender = true;
const books = await fetchBooks();
const currentBookCount = books.currentYear.length;
const bookData = books.all
.filter((book) => book.status === "started")
.reverse();
const { globals } = await fetchGlobalData(Astro);
const title = "Currently reading";
const description = "Here's what I'm reading at the moment.";
const updated = new Date().toISOString();
const currentYear = new Date().getFullYear();
---
<Layout
pageTitle={title}
description={description}
currentUrl={Astro.url.pathname}
ogImage={bookData[0].image}
>
<h2 class="page-title">{title}</h2>
<p>
{description} I've finished <strong class="highlight-text"
>{currentBookCount} books</strong
> this year.
</p>
<p set:html={bookYearLinks(books.years)} />
<Rss
url="/feeds/books.xml"
text="Subscribe to my books feed or follow along on this page"
/>
<hr />
{
bookData.map((book) => (
<article class="book-entry" key={book.url}>
<a href={book.url}>
<img
srcset={`
${globals.cdn_url}${book.image}?class=verticalsm&type=webp 200w,
${globals.cdn_url}${book.image}?class=verticalmd&type=webp 400w
`}
sizes="(max-width: 450px) 200px, 400px"
src={`${globals.cdn_url}${book.image}?class=verticalsm&type=webp`}
alt={`${book.title} by ${book.authors}`}
loading="lazy"
decoding="async"
width="200"
height="307"
/>
</a>
<div class="media-meta">
<a href={book.url}>
<span class="title">
<strong>{book.title}</strong>
</span>
</a>
{book.author && <span class="sub-meta">By {book.author}</span>}
{book.progress && <ProgressBar percentage={`${book.progress}%`} />}
{book.description && (
<div
class="description"
set:html={htmlTruncate(md(book.description))}
/>
)}
</div>
</article>
))
}
</Layout>

View file

@ -0,0 +1,63 @@
---
import Layout from "@layouts/Layout.astro";
import Grid from "@components/media/Grid.astro";
import icons from "@cdransf/astro-tabler-icons";
import {
filterBooksByStatus,
findFavoriteBooks,
mediaLinks,
} from "@utils/helpers/media.js";
import { fetchBooks } from "@utils/data/books.js";
export const prerender = true;
export async function getStaticPaths() {
const books = await fetchBooks();
return books.years.map((yearData) => ({
params: { year: String(yearData.value) },
}));
}
const { IconArrowLeft } = icons;
const books = await fetchBooks();
const { year } = Astro.params;
const yearData = books.years.find((y) => y.value === parseInt(year, 10));
if (!yearData) return Astro.redirect("/404", 404);
const bookData = filterBooksByStatus(yearData.data, "finished");
const bookDataFavorites = findFavoriteBooks(bookData);
const favoriteBooks = mediaLinks(bookDataFavorites, "book", 5);
const currentYear = new Date().getFullYear();
const isCurrentYear = parseInt(year, 10) === currentYear;
const pageTitle = `${year} / Books`;
const description = isCurrentYear
? `I've finished ${bookData.length} books this year.`
: `I finished ${bookData.length} books in ${year}.`;
const intro = isCurrentYear
? `
I've finished <strong class="highlight-text">${bookData.length} books</strong> this year.
${favoriteBooks ? ` Among my favorites are ${favoriteBooks}.` : ""}
`
: `
I finished <strong class="highlight-text">${bookData.length} books</strong> in
<strong class="highlight-text">${year}</strong>.
${favoriteBooks ? ` Among my favorites were ${favoriteBooks}.` : ""}
`;
---
<Layout
pageTitle={pageTitle}
description={description}
fullUrl={Astro.url.pathname}
ogImage={bookData[0]?.image || ""}
>
<a href="/books" class="back-link">
<div set:html={IconArrowLeft({ size: 18 })}/> Back to books
</a>
<h2 class="page-title">{year} / Books</h2>
<div set:html={intro} />
<hr />
<Grid data={bookData} shape="vertical" count={200} loading="eager" />
</Layout>

View file

@ -0,0 +1,23 @@
import rss from "@astrojs/rss";
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchActivity } from "@utils/data/activity.js";
export const prerender = true;
export async function GET() {
const globals = await fetchGlobals();
const activity = await fetchActivity();
return rss({
title: "coryd.dev activity feed",
description: "The latest activity from Cory Dransfeldt.",
site: globals.url,
stylesheet: `${globals.url}/feeds/style.xsl`,
items: activity.map((item) => ({
title: item.feed.title,
pubDate: item.feed.date,
link: item.feed.url,
description: item.feed.description,
})),
});
}

View file

@ -0,0 +1,24 @@
import rss from "@astrojs/rss";
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchBooks } from "@utils/data/books.js";
import { escapeHtml, md } from "@utils/helpers/general.js";
export const prerender = true;
export async function GET() {
const globals = await fetchGlobals();
const books = await fetchBooks();
return rss({
title: "coryd.dev books feed",
description: "The latest books Cory Dransfeldt has read.",
site: globals.url,
stylesheet: `${globals.url}/feeds/style.xsl`,
items: books.feed.slice(0, 20).map((book) => ({
title: book.feed.title,
pubDate: book.feed.date,
link: book.feed.url,
description: escapeHtml(md(book.feed.description)),
})),
});
}

View file

@ -0,0 +1,24 @@
import rss from "@astrojs/rss";
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchLinks } from "@utils/data/links.js";
export const prerender = true;
export async function GET() {
const globals = await fetchGlobals();
const links = await fetchLinks();
return rss({
title: "coryd.dev links feed",
description:
"These are links I've liked or otherwise found interesting. They're all added manually, after having been read and, I suppose, properly considered.",
site: globals.url,
stylesheet: `${globals.url}/feeds/style.xsl`,
items: links.slice(0, 20).map((link) => ({
title: link.feed.title,
pubDate: link.feed.date,
link: link.feed.url,
description: link.feed.description,
})),
});
}

View file

@ -0,0 +1,24 @@
import rss from "@astrojs/rss";
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchMovies } from "@utils/data/movies.js";
import { escapeHtml, md } from "@utils/helpers/general.js";
export const prerender = true;
export async function GET() {
const globals = await fetchGlobals();
const movies = await fetchMovies();
return rss({
title: "coryd.dev movies feed",
description: "The latest movie Cory Dransfeldt has watched.",
site: globals.url,
stylesheet: `${globals.url}/feeds/style.xsl`,
items: movies.feed.slice(0, 20).map((movie) => ({
title: movie.feed.title,
pubDate: movie.feed.date,
link: movie.feed.url,
description: escapeHtml(md(movie.feed.description)),
})),
});
}

View file

@ -0,0 +1,24 @@
import rss from "@astrojs/rss";
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchAllPosts } from "@utils/data/posts.js";
import { escapeHtml, md } from "@utils/helpers/general.js";
export const prerender = true;
export async function GET() {
const globals = await fetchGlobals();
const posts = await fetchAllPosts();
return rss({
title: "coryd.dev posts feed",
description: "The latest posts from Cory Dransfeldt.",
site: globals.url,
stylesheet: `${globals.url}/feeds/style.xsl`,
items: posts.slice(0, 20).map((post) => ({
title: post.feed.title,
pubDate: post.feed.date,
link: post.feed.url,
description: escapeHtml(md(post.feed.description)),
})),
});
}

View file

@ -0,0 +1,22 @@
import rss from "@astrojs/rss";
import { fetchGlobals } from "@utils/data/globals.js";
import { fetchSyndication } from "@utils/data/syndication.js";
export const prerender = true;
export async function GET() {
const globals = await fetchGlobals();
const syndication = await fetchSyndication();
return rss({
title: "coryd.dev syndication feed",
description: "The feed that gets syndicated out to Mastodon",
site: globals.url,
items: syndication.map((item) => ({
title: item.syndication.title,
pubDate: item.syndication.date,
link: item.syndication.url,
description: item.syndication.description,
})),
});
}

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

@ -0,0 +1,31 @@
import { fetchGlobals } from '@utils/data/globals.js';
export const prerender = true;
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 });
}
}

17
src/pages/index.astro Normal file
View file

@ -0,0 +1,17 @@
---
import Layout from "@layouts/Layout.astro";
import Intro from "@components/home/Intro.astro";
import RecentActivity from "@components/home/RecentActivity.astro";
import RecentPosts from "@components/home/RecentPosts.astro";
import { fetchGlobalData } from "@utils/data/global/index.js";
export const prerender = true;
const { globals } = await fetchGlobalData(Astro);
---
<Layout>
<Intro intro={globals.intro} />
<RecentActivity />
<RecentPosts />
</Layout>

View file

@ -0,0 +1,76 @@
---
import Layout from "@layouts/Layout.astro";
import Paginator from "@components/nav/Paginator.astro";
import Rss from "@components/blocks/banners/Rss.astro";
import { fetchLinks } from "@utils/data/links.js";
export const prerender = true;
export const getStaticPaths = async ({ paginate }) => {
const links = await fetchLinks();
return paginate(links, {
pageSize: 30,
});
};
const { page } = Astro.props;
const paginatedLinks = page.data;
const pagination = {
currentPage: page.currentPage,
totalPages: page.lastPage,
hasPrevious: page.currentPage > 1,
hasNext: page.currentPage < page.lastPage,
previousPage: page.url.prev || null,
nextPage: page.url.next || null,
pages: Array.from({ length: page.lastPage }, (_, i) => ({
number: i + 1,
href: i === 0 ? `/links` : `/links/${i + 1}`,
})),
};
const pageTitle =
pagination.currentPage === 1
? "Links"
: `Links / page ${pagination.currentPage}`;
const description =
"These are links I've liked or otherwise found interesting. They're all added manually, after having been read and, I suppose, properly considered.";
---
<Layout
pageTitle={pageTitle}
description={description}
currentUrl={Astro.url.pathname}
>
{
pagination.currentPage === 1 && (
<>
<h2 class="page-title">{pageTitle}</h2>
<p>{description}</p>
<Rss
url="/feeds/links.xml"
text="Subscribe to my links feed or follow along on this page"
/>
<hr />
</>
)
}
<div class="link-grid">
{
paginatedLinks.map((link) => (
<div class="link-box">
<a href={link.link} title={link.title}>
<strong>{link.title}</strong>
</a>
{link.author && (
<>
{" via "}
<a href={link.author.url}>{link.author.name}</a>
</>
)}
</div>
))
}
</div>
<Paginator pagination={pagination} />
</Layout>

Some files were not shown because too many files have changed in this diff Show more