diff --git a/package-lock.json b/package-lock.json
index 3366c47..e381d06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "coryd.dev",
- "version": "1.8.0",
+ "version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "coryd.dev",
- "version": "1.8.0",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
"html-minifier-terser": "7.2.0",
@@ -1066,9 +1066,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001713",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
- "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
+ "version": "1.0.30001714",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz",
+ "integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==",
"dev": true,
"funding": [
{
@@ -2986,9 +2986,9 @@
"license": "BSD-3-Clause"
},
"node_modules/morphdom": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz",
- "integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==",
+ "version": "2.7.5",
+ "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.5.tgz",
+ "integrity": "sha512-z6bfWFMra7kBqDjQGHud1LSXtq5JJC060viEkQFMBX6baIecpkNr2Ywrn2OQfWP3rXiNFQRPoFjD8/TvJcWcDg==",
"dev": true,
"license": "MIT"
},
@@ -4682,9 +4682,9 @@
}
},
"node_modules/tr46": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz",
- "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 8002e2a..74273a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "coryd.dev",
- "version": "1.8.0",
+ "version": "2.0.0",
"description": "The source for my personal site. Built using 11ty (and other tools).",
"type": "module",
"engines": {
diff --git a/queries/functions/get_tagged_content.psql b/queries/functions/get_tagged_content.psql
new file mode 100644
index 0000000..1484168
--- /dev/null
+++ b/queries/functions/get_tagged_content.psql
@@ -0,0 +1,38 @@
+CREATE OR REPLACE FUNCTION get_tagged_content(
+ tag_query TEXT,
+ page_size INTEGER DEFAULT 20,
+ page_offset INTEGER DEFAULT 0
+)
+RETURNS TABLE (
+ tag TEXT,
+ title TEXT,
+ url TEXT,
+ content_date TIMESTAMP,
+ author JSON,
+ rating TEXT,
+ featured BOOLEAN,
+ tags TEXT[],
+ type TEXT,
+ label TEXT,
+ total_count BIGINT
+) AS $$
+BEGIN
+ RETURN QUERY
+ SELECT
+ t.tag,
+ t.title,
+ t.url,
+ t.content_date,
+ t.author,
+ t.rating,
+ t.featured,
+ t.tags,
+ t.type,
+ t.label,
+ COUNT(*) OVER() AS total_count
+ FROM optimized_tagged_content t
+ WHERE LOWER(TRIM(t.tag)) = LOWER(TRIM(tag_query))
+ ORDER BY content_date DESC NULLS LAST
+ LIMIT page_size OFFSET page_offset;
+END;
+$$ LANGUAGE plpgsql STABLE;
diff --git a/queries/views/feeds/recent_activity.psql b/queries/views/feeds/recent_activity.psql
index 723931c..2edfdbb 100644
--- a/queries/views/feeds/recent_activity.psql
+++ b/queries/views/feeds/recent_activity.psql
@@ -6,6 +6,7 @@ WITH activity_data AS (
p.content AS description,
p.url AS url,
p.featured AS featured,
+ p.tags::TEXT[],
NULL AS author,
NULL AS image,
NULL AS rating,
@@ -26,6 +27,7 @@ WITH activity_data AS (
l.description,
l.link AS url,
NULL AS featured,
+ l.tags::TEXT[],
l.author,
NULL AS image,
NULL AS rating,
@@ -48,6 +50,7 @@ WITH activity_data AS (
b.description,
b.url AS url,
NULL AS featured,
+ b.tags::TEXT[],
NULL AS author,
b.image,
b.rating,
@@ -71,6 +74,7 @@ WITH activity_data AS (
m.description,
m.url AS url,
NULL AS featured,
+ m.tags::TEXT[],
NULL AS author,
m.image,
m.rating,
@@ -92,6 +96,7 @@ WITH activity_data AS (
c.concert_notes AS description,
NULL AS url,
NULL AS featured,
+ NULL AS tags,
NULL AS author,
NULL AS image,
NULL AS rating,
diff --git a/queries/views/feeds/sitemap.psql b/queries/views/feeds/sitemap.psql
index b4d0d65..8b6c492 100644
--- a/queries/views/feeds/sitemap.psql
+++ b/queries/views/feeds/sitemap.psql
@@ -39,6 +39,10 @@ WITH sitemap_data AS (
ss.slug AS url
FROM
static_slugs ss
+ UNION ALL
+ SELECT CONCAT('/tags/', LOWER(REPLACE(tag, ' ', '-'))) AS url
+ FROM optimized_all_tags
+ WHERE tag IS NOT NULL AND TRIM(tag) <> ''
)
SELECT
url
diff --git a/queries/views/feeds/tagged_content.psql b/queries/views/feeds/tagged_content.psql
new file mode 100644
index 0000000..b4b1337
--- /dev/null
+++ b/queries/views/feeds/tagged_content.psql
@@ -0,0 +1,68 @@
+CREATE OR REPLACE VIEW optimized_tagged_content AS
+SELECT
+ unnest(p.tags) AS tag,
+ p.title::TEXT,
+ p.url::TEXT,
+ p.date::timestamp WITHOUT TIME ZONE AS content_date,
+ NULL AS author,
+ NULL::TEXT AS rating,
+ p.featured,
+ p.tags::TEXT[],
+ 'article'::TEXT AS type,
+ 'Post'::TEXT AS label
+FROM optimized_posts p
+UNION ALL
+SELECT
+ unnest(l.tags) AS tag,
+ l.title::TEXT,
+ l.link::TEXT AS url,
+ l.date::timestamp WITHOUT TIME ZONE AS content_date,
+ l.author,
+ NULL::TEXT AS rating,
+ NULL::BOOLEAN AS featured,
+ l.tags::TEXT[],
+ 'link'::TEXT AS type,
+ 'Link'::TEXT AS label
+FROM optimized_links l
+UNION ALL
+SELECT
+ unnest(b.tags) AS tag,
+ b.title::TEXT,
+ b.url::TEXT,
+ b.date_finished::timestamp WITHOUT TIME ZONE AS content_date,
+ NULL AS author,
+ b.rating::TEXT,
+ NULL::BOOLEAN AS featured,
+ b.tags::TEXT[],
+ 'books'::TEXT AS type,
+ 'Book'::TEXT AS label
+FROM optimized_books b
+WHERE LOWER(b.status) = 'finished'
+UNION ALL
+SELECT
+ unnest(m.tags) AS tag,
+ m.title::TEXT,
+ m.url::TEXT,
+ m.last_watched::timestamp WITHOUT TIME ZONE AS content_date,
+ NULL AS author,
+ m.rating::TEXT,
+ NULL::BOOLEAN AS featured,
+ m.tags::TEXT[],
+ 'movies'::TEXT AS type,
+ 'Movie'::TEXT AS label
+FROM optimized_movies m
+WHERE m.last_watched IS NOT NULL
+UNION ALL
+SELECT
+ unnest(s.tags) AS tag,
+ s.title::TEXT,
+ s.url::TEXT,
+ s.last_watched_at::timestamp WITHOUT TIME ZONE AS content_date,
+ NULL AS author,
+ NULL::TEXT AS rating,
+ NULL::BOOLEAN AS featured,
+ s.tags::TEXT[],
+ 'tv'::TEXT AS type,
+ 'Show'::TEXT AS label
+FROM optimized_shows s
+WHERE s.last_watched_at IS NOT NULL;
diff --git a/queries/views/feeds/tags.psql b/queries/views/feeds/tags.psql
new file mode 100644
index 0000000..3cf52bf
--- /dev/null
+++ b/queries/views/feeds/tags.psql
@@ -0,0 +1,7 @@
+CREATE OR REPLACE VIEW optimized_all_tags AS
+SELECT
+ tag,
+ COUNT(*) AS uses
+FROM optimized_tagged_content
+GROUP BY tag
+ORDER BY tag ASC;
diff --git a/server/utils/icons.php b/server/utils/icons.php
index f5054d9..f95159b 100644
--- a/server/utils/icons.php
+++ b/server/utils/icons.php
@@ -3,11 +3,14 @@
function getTablerIcon($iconName, $class = '', $size = 24)
{
$icons = [
+ 'arrow-left' => ' ',
+ 'arrow-right' => ' ',
'article' => ' ',
'books' => ' ',
'device-tv-old' => ' ',
'headphones' => ' ',
- 'movie' => ' '
+ 'movie' => ' ',
+ 'star' => ' '
];
return $icons[$iconName] ?? '[Missing: ' . htmlspecialchars($iconName) . '] ';
diff --git a/server/utils/init.php b/server/utils/init.php
index c1650d2..4894fa5 100644
--- a/server/utils/init.php
+++ b/server/utils/init.php
@@ -1,5 +1,7 @@
diff --git a/server/utils/paginator.php b/server/utils/paginator.php
new file mode 100644
index 0000000..c5d2b44
--- /dev/null
+++ b/server/utils/paginator.php
@@ -0,0 +1,44 @@
+
+
+
+
+
+ ';
+ foreach ($tags as $tag) {
+ $slug = strtolower(trim($tag));
+ echo '#' . htmlspecialchars($slug) . ' ';
+ }
+ echo '';
+}
diff --git a/src/assets/scripts/components/select-pagination.js b/src/assets/scripts/components/select-pagination.js
index 7ea1da0..4d6dcdb 100644
--- a/src/assets/scripts/components/select-pagination.js
+++ b/src/assets/scripts/components/select-pagination.js
@@ -8,7 +8,7 @@ class SelectPagination extends HTMLElement {
}
get baseIndex() {
- return this.getAttribute('data-base-index') || 0
+ return parseInt(this.getAttribute('data-base-index') || '0', 10)
}
connectedCallback() {
@@ -17,10 +17,11 @@ class SelectPagination extends HTMLElement {
this.attachShadow({ mode: 'open' }).appendChild(document.createElement('slot'))
const uriSegments = window.location.pathname.split('/').filter(Boolean)
- let pageNumber = this.extractPageNumber(uriSegments) || 0
+ let pageNumber = this.extractPageNumber(uriSegments)
+ if (pageNumber === null) pageNumber = this.baseIndex
this.control = this.querySelector('select')
- this.control.value = pageNumber
+ this.control.value = pageNumber.toString()
this.control.addEventListener('change', (event) => {
pageNumber = parseInt(event.target.value)
const updatedUrlSegments = this.updateUrlSegments(uriSegments, pageNumber)
@@ -34,13 +35,16 @@ class SelectPagination extends HTMLElement {
}
updateUrlSegments(segments, pageNumber) {
- if (!isNaN(segments[segments.length - 1])) {
+ const lastIsPage = !isNaN(segments[segments.length - 1])
+
+ if (lastIsPage) {
segments[segments.length - 1] = pageNumber.toString()
} else {
segments.push(pageNumber.toString())
}
- if (pageNumber === parseInt(this.baseIndex)) segments.pop()
+ if (pageNumber === this.baseIndex) segments.pop()
+
return segments
}
}
diff --git a/src/assets/styles/base/index.css b/src/assets/styles/base/index.css
index 7716d07..e240f97 100644
--- a/src/assets/styles/base/index.css
+++ b/src/assets/styles/base/index.css
@@ -257,10 +257,7 @@ a {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
-
- &:not(:has(+ h1, + h2, + h3)) {
- margin-bottom: var(--spacing-base);
- }
+ margin-bottom: var(--spacing-base);
&:is(:hover, :focus, :active) svg {
transform: var(--transform-icon-default);
@@ -350,7 +347,13 @@ hr {
time {
color: var(--gray-dark);
font-size: var(--font-size-sm);
- line-height: var(--sizing-sm);
+}
+
+.tags {
+ font-size: var(--font-size-sm);
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
}
article {
@@ -366,6 +369,10 @@ article {
h3 {
margin-top: 0;
+
+ &:has(+ .tags) {
+ margin-bottom: 0;
+ }
}
aside {
diff --git a/src/assets/styles/base/vars.css b/src/assets/styles/base/vars.css
index ae89673..a13e31d 100644
--- a/src/assets/styles/base/vars.css
+++ b/src/assets/styles/base/vars.css
@@ -158,7 +158,7 @@
/* transforms */
--transform-icon-default: rotate(0);
- --transform-icon-tilt: rotate(7.5deg);
+ --transform-icon-tilt: rotate(10deg);
@media (prefers-reduced-motion) {
--transform-icon-tilt: var(--transform-icon-default);
diff --git a/src/assets/styles/components/dialog.css b/src/assets/styles/components/dialog.css
index 8554801..58df183 100644
--- a/src/assets/styles/components/dialog.css
+++ b/src/assets/styles/components/dialog.css
@@ -22,11 +22,6 @@ dialog {
transition: color var(--transition-duration-default) var(--transition-ease-in-out),
transform var(--transition-duration-default) var(--transition-ease-in-out);
- svg {
- --sizing-svg: var(--sizing-svg-base);
- }
-
-
&:is(:hover, :focus, :active) {
color: var(--link-color-hover);
@@ -77,6 +72,10 @@ dialog {
left: var(--sizing-full);
background: var(--background-color);
border-radius: var(--border-radius-full);
+
+ svg {
+ --sizing-svg: var(--sizing-svg-base);
+ }
}
}
diff --git a/src/assets/styles/components/forms.css b/src/assets/styles/components/forms.css
index de4434f..323e14f 100644
--- a/src/assets/styles/components/forms.css
+++ b/src/assets/styles/components/forms.css
@@ -44,10 +44,6 @@ button svg,
label svg {
stroke: var(--section-color, var(--accent-color));
cursor: pointer;
-
- &:is(:hover, :focus, :active) {
- stroke: var(--accent-color-hover);
- }
}
summary {
diff --git a/src/data/tags.js b/src/data/tags.js
new file mode 100644
index 0000000..ea7d4a2
--- /dev/null
+++ b/src/data/tags.js
@@ -0,0 +1,36 @@
+import EleventyFetch from "@11ty/eleventy-fetch";
+
+const { POSTGREST_URL, POSTGREST_API_KEY } = process.env;
+
+export default async function () {
+ const res = await EleventyFetch(`${POSTGREST_URL}/optimized_all_tags?order=tag.asc`, {
+ duration: "1h",
+ type: "json",
+ fetchOptions: {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${POSTGREST_API_KEY}`,
+ },
+ },
+ });
+ const tags = await res;
+ const groupedMap = new Map();
+
+ for (const tag of tags) {
+ const letter = /^[a-zA-Z]/.test(tag.tag) ? tag.tag[0].toUpperCase() : "#";
+
+ if (!groupedMap.has(letter)) groupedMap.set(letter, []);
+
+ groupedMap.get(letter).push(tag);
+ }
+
+ const grouped = [...groupedMap.entries()]
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([letter, tags]) => ({
+ letter,
+ tags: tags.sort((a, b) => a.tag.localeCompare(b.tag)),
+ }));
+
+ return grouped;
+}
diff --git a/src/includes/blocks/tags.liquid b/src/includes/blocks/tags.liquid
new file mode 100644
index 0000000..2d46c5e
--- /dev/null
+++ b/src/includes/blocks/tags.liquid
@@ -0,0 +1,7 @@
+{% if tags %}
+
+{% endif %}
diff --git a/src/includes/fetchers/tags.php.liquid b/src/includes/fetchers/tags.php.liquid
new file mode 100644
index 0000000..4ec6785
--- /dev/null
+++ b/src/includes/fetchers/tags.php.liquid
@@ -0,0 +1,109 @@
+connect('127.0.0.1', 6379);
+ $useRedis = true;
+ }
+} catch (Exception $e) {
+ error_log("Redis not available: " . $e->getMessage());
+}
+
+if ($useRedis && $redis->exists($cacheKey)) {
+ $tagged = json_decode($redis->get($cacheKey), true);
+} else {
+ $client = new Client();
+ try {
+ $response = $client->post($postgrestUrl . '/rpc/get_tagged_content', [
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer {$postgrestApiKey}",
+ ],
+ 'json' => [
+ 'tag_query' => $tag,
+ 'page_size' => $pageSize,
+ 'page_offset' => $offset
+ ]
+ ]);
+
+ $tagged = json_decode($response->getBody(), true);
+ if ($useRedis) {
+ $redis->setex($cacheKey, 3600, json_encode($tagged));
+ }
+ } catch (Exception $e) {
+ error_log($e->getMessage());
+ echo file_get_contents(__DIR__ . '/../404/index.html');
+ exit();
+ }
+}
+
+if (!$tagged || count($tagged) === 0) {
+ echo file_get_contents(__DIR__ . '/../404/index.html');
+ exit();
+}
+
+$totalCount = $tagged[0]['total_count'] ?? 0;
+$totalPages = max(ceil($totalCount / $pageSize), 1);
+$pagination = [
+ 'pageNumber' => $page,
+ 'pages' => range(1, $totalPages),
+ 'href' => [
+ 'previous' => $page > 1 ? "/tags/{$tag}/" . ($page - 1) : null,
+ 'next' => $page < $totalPages ? "/tags/{$tag}/" . ($page + 1) : null
+ ],
+ 'links' => range(1, $totalPages)
+];
+
+$pageTitle = "#" . strtolower(ucfirst($tag)) . "";
+$pageDescription = "All content tagged with #" . strtolower(ucfirst($tag)) . ".";
+$fullUrl = "https://www.coryd.dev" . $requestUri;
+
+ob_start();
+
+header("Cache-Control: public, max-age=3600");
+header("Expires: " . gmdate("D, d M Y H:i:s", time() + 3600) . " GMT");
+
+?>
diff --git a/src/includes/home/recent-activity.liquid b/src/includes/home/recent-activity.liquid
index 2e31743..da56e5d 100644
--- a/src/includes/home/recent-activity.liquid
+++ b/src/includes/home/recent-activity.liquid
@@ -13,11 +13,11 @@
{{ item.content_date | date:"%B %e, %Y" }}
• {{ item.label }}
-
+
{%- if item.notes -%}
{% assign notes = item.notes | prepend: "### Notes\n" | markdown %}
- • {% render "blocks/dialog.liquid",
- label:"Notes",
+ {% render "blocks/dialog.liquid",
+ icon:"info-circle",
content:notes,
id:item.content_date
%}
@@ -54,6 +54,9 @@
{%- endif -%}
{%- endif -%}
+ {% render "blocks/tags.liquid",
+ 18 tags:item.tags
+ 19 %}
{%- endfor -%}
diff --git a/src/includes/metadata/dynamic.php.liquid b/src/includes/metadata/dynamic.php.liquid
index 49b75f1..5d320de 100644
--- a/src/includes/metadata/dynamic.php.liquid
+++ b/src/includes/metadata/dynamic.php.liquid
@@ -5,3 +5,11 @@
" />
+
+
+
+
+
+
+
+
diff --git a/src/includes/metadata/index.liquid b/src/includes/metadata/index.liquid
index dea6c6e..6b056fb 100644
--- a/src/includes/metadata/index.liquid
+++ b/src/includes/metadata/index.liquid
@@ -29,6 +29,8 @@
{% render "fetchers/movie.php.liquid" %}
{%- when 'show' -%}
{% render "fetchers/show.php.liquid" %}
+ {%- when 'tags' -%}
+ {% render "fetchers/tags.php.liquid" %}
{%- when 'blog' -%}
{%- assign pageTitle = post.title -%}
{%- assign pageDescription = post.description -%}
diff --git a/src/meta/htaccess.liquid b/src/meta/htaccess.liquid
index 665be3f..df25c50 100644
--- a/src/meta/htaccess.liquid
+++ b/src/meta/htaccess.liquid
@@ -25,9 +25,7 @@ ErrorDocument 404 /404/index.html
ErrorDocument 429 /429/index.html
ErrorDocument 500 /500/index.html
-# media routing
-
-# media routing
+# dynamic page routing
## artists
RewriteRule ^music/artists/([^/]+)/?$ music/artists/index.php [L]
@@ -46,6 +44,9 @@ RewriteRule ^watching/shows/([^/]+)/?$ watching/shows/index.php [L]
## genres
RewriteRule ^music/genres/([^/]+)/?$ music/genres/index.php [L]
+## tags
+RewriteRule ^tags/([^/]+)(?:/([0-9]+))?/?$ tags/index.php [L]
+
{% for redirect in redirects -%}
Redirect {{ redirect.status_code | default: "301" }} {{ redirect.source_url }} {{ redirect.destination_url }}
{% endfor -%}
diff --git a/src/pages/dynamic/artist.php.liquid b/src/pages/dynamic/artist.php.liquid
index 6175679..3f4d43b 100644
--- a/src/pages/dynamic/artist.php.liquid
+++ b/src/pages/dynamic/artist.php.liquid
@@ -23,7 +23,7 @@ schema: artist
height="200"
/>