feat(tags): this adds support for post, link, book, show and movie tags with a tag list view and per tag pages
This commit is contained in:
parent
3d866262ca
commit
6fdc0b56b9
35 changed files with 500 additions and 70 deletions
22
package-lock.json
generated
22
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "1.8.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "1.8.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
|
@ -1066,9 +1066,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001713",
|
"version": "1.0.30001714",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz",
|
||||||
"integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==",
|
"integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -2986,9 +2986,9 @@
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/morphdom": {
|
"node_modules/morphdom": {
|
||||||
"version": "2.7.4",
|
"version": "2.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.5.tgz",
|
||||||
"integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==",
|
"integrity": "sha512-z6bfWFMra7kBqDjQGHud1LSXtq5JJC060viEkQFMBX6baIecpkNr2Ywrn2OQfWP3rXiNFQRPoFjD8/TvJcWcDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
@ -4682,9 +4682,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
"integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==",
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "coryd.dev",
|
"name": "coryd.dev",
|
||||||
"version": "1.8.0",
|
"version": "2.0.0",
|
||||||
"description": "The source for my personal site. Built using 11ty (and other tools).",
|
"description": "The source for my personal site. Built using 11ty (and other tools).",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
38
queries/functions/get_tagged_content.psql
Normal file
38
queries/functions/get_tagged_content.psql
Normal file
|
@ -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;
|
|
@ -6,6 +6,7 @@ WITH activity_data AS (
|
||||||
p.content AS description,
|
p.content AS description,
|
||||||
p.url AS url,
|
p.url AS url,
|
||||||
p.featured AS featured,
|
p.featured AS featured,
|
||||||
|
p.tags::TEXT[],
|
||||||
NULL AS author,
|
NULL AS author,
|
||||||
NULL AS image,
|
NULL AS image,
|
||||||
NULL AS rating,
|
NULL AS rating,
|
||||||
|
@ -26,6 +27,7 @@ WITH activity_data AS (
|
||||||
l.description,
|
l.description,
|
||||||
l.link AS url,
|
l.link AS url,
|
||||||
NULL AS featured,
|
NULL AS featured,
|
||||||
|
l.tags::TEXT[],
|
||||||
l.author,
|
l.author,
|
||||||
NULL AS image,
|
NULL AS image,
|
||||||
NULL AS rating,
|
NULL AS rating,
|
||||||
|
@ -48,6 +50,7 @@ WITH activity_data AS (
|
||||||
b.description,
|
b.description,
|
||||||
b.url AS url,
|
b.url AS url,
|
||||||
NULL AS featured,
|
NULL AS featured,
|
||||||
|
b.tags::TEXT[],
|
||||||
NULL AS author,
|
NULL AS author,
|
||||||
b.image,
|
b.image,
|
||||||
b.rating,
|
b.rating,
|
||||||
|
@ -71,6 +74,7 @@ WITH activity_data AS (
|
||||||
m.description,
|
m.description,
|
||||||
m.url AS url,
|
m.url AS url,
|
||||||
NULL AS featured,
|
NULL AS featured,
|
||||||
|
m.tags::TEXT[],
|
||||||
NULL AS author,
|
NULL AS author,
|
||||||
m.image,
|
m.image,
|
||||||
m.rating,
|
m.rating,
|
||||||
|
@ -92,6 +96,7 @@ WITH activity_data AS (
|
||||||
c.concert_notes AS description,
|
c.concert_notes AS description,
|
||||||
NULL AS url,
|
NULL AS url,
|
||||||
NULL AS featured,
|
NULL AS featured,
|
||||||
|
NULL AS tags,
|
||||||
NULL AS author,
|
NULL AS author,
|
||||||
NULL AS image,
|
NULL AS image,
|
||||||
NULL AS rating,
|
NULL AS rating,
|
||||||
|
|
|
@ -39,6 +39,10 @@ WITH sitemap_data AS (
|
||||||
ss.slug AS url
|
ss.slug AS url
|
||||||
FROM
|
FROM
|
||||||
static_slugs ss
|
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
|
SELECT
|
||||||
url
|
url
|
||||||
|
|
68
queries/views/feeds/tagged_content.psql
Normal file
68
queries/views/feeds/tagged_content.psql
Normal file
|
@ -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;
|
7
queries/views/feeds/tags.psql
Normal file
7
queries/views/feeds/tags.psql
Normal file
|
@ -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;
|
|
@ -3,11 +3,14 @@
|
||||||
function getTablerIcon($iconName, $class = '', $size = 24)
|
function getTablerIcon($iconName, $class = '', $size = 24)
|
||||||
{
|
{
|
||||||
$icons = [
|
$icons = [
|
||||||
|
'arrow-left' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" /></svg>',
|
||||||
|
'arrow-right' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M13 18l6 -6" /><path d="M13 6l6 6" /></svg>',
|
||||||
'article' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-article"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 8h10" /><path d="M7 12h10" /><path d="M7 16h10" /></svg>',
|
'article' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-article"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 8h10" /><path d="M7 12h10" /><path d="M7 16h10" /></svg>',
|
||||||
'books' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-books"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M9 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M5 8h4" /><path d="M9 16h4" /><path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041z" /><path d="M14 9l4 -1" /><path d="M16 16l3.923 -.98" /></svg>',
|
'books' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-books"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M9 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M5 8h4" /><path d="M9 16h4" /><path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041z" /><path d="M14 9l4 -1" /><path d="M16 16l3.923 -.98" /></svg>',
|
||||||
'device-tv-old' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-tv-old"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M16 3l-4 4l-4 -4" /><path d="M15 7v13" /><path d="M18 15v.01" /><path d="M18 12v.01" /></svg>',
|
'device-tv-old' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-tv-old"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M16 3l-4 4l-4 -4" /><path d="M15 7v13" /><path d="M18 15v.01" /><path d="M18 12v.01" /></svg>',
|
||||||
'headphones' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-headphones"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M15 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M4 15v-3a8 8 0 0 1 16 0v3" /></svg>',
|
'headphones' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-headphones"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M15 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M4 15v-3a8 8 0 0 1 16 0v3" /></svg>',
|
||||||
'movie' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-movie"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M8 4l0 16" /><path d="M16 4l0 16" /><path d="M4 8l4 0" /><path d="M4 16l4 0" /><path d="M4 12l16 0" /><path d="M16 8l4 0" /><path d="M16 16l4 0" /></svg>'
|
'movie' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-movie"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M8 4l0 16" /><path d="M16 4l0 16" /><path d="M4 8l4 0" /><path d="M4 16l4 0" /><path d="M4 12l16 0" /><path d="M16 8l4 0" /><path d="M16 16l4 0" /></svg>',
|
||||||
|
'star' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-star"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" /></svg>'
|
||||||
];
|
];
|
||||||
|
|
||||||
return $icons[$iconName] ?? '<span class="icon-placeholder">[Missing: ' . htmlspecialchars($iconName) . ']</span>';
|
return $icons[$iconName] ?? '<span class="icon-placeholder">[Missing: ' . htmlspecialchars($iconName) . ']</span>';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
require_once "icons.php";
|
require_once "icons.php";
|
||||||
require_once "media.php";
|
require_once "media.php";
|
||||||
|
require_once "paginator.php";
|
||||||
require_once "strings.php";
|
require_once "strings.php";
|
||||||
|
require_once "tags.php";
|
||||||
?>
|
?>
|
||||||
|
|
44
server/utils/paginator.php
Normal file
44
server/utils/paginator.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/icons.php';
|
||||||
|
|
||||||
|
function renderPaginator(array $pagination, int $totalPages): void {
|
||||||
|
if (!$pagination || $totalPages <= 1) return;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<script type="module" src="/assets/scripts/components/select-pagination.js" defer></script>
|
||||||
|
<nav aria-label="Pagination" class="pagination">
|
||||||
|
<?php if (!empty($pagination['href']['previous'])): ?>
|
||||||
|
<a href="<?= $pagination['href']['previous'] ?>" aria-label="Previous page">
|
||||||
|
<?= getTablerIcon('arrow-left') ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span><?= getTablerIcon('arrow-left') ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<select-pagination data-base-index="1">
|
||||||
|
<select class="client-side" aria-label="Page selection">
|
||||||
|
<?php foreach ($pagination['pages'] as $i): ?>
|
||||||
|
<option value="<?= $i ?>" <?= ($pagination['pageNumber'] === $i) ? 'selected' : '' ?>>
|
||||||
|
<?= $i ?> of <?= $totalPages ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<noscript>
|
||||||
|
<p>
|
||||||
|
<span aria-current="page"><?= $pagination['pageNumber'] ?></span> of <?= $totalPages ?>
|
||||||
|
</p>
|
||||||
|
</noscript>
|
||||||
|
</select-pagination>
|
||||||
|
|
||||||
|
<?php if (!empty($pagination['href']['next'])): ?>
|
||||||
|
<a href="<?= $pagination['href']['next'] ?>" aria-label="Next page">
|
||||||
|
<?= getTablerIcon('arrow-right') ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span><?= getTablerIcon('arrow-right') ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
}
|
12
server/utils/tags.php
Normal file
12
server/utils/tags.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
function renderTags(array $tags): void {
|
||||||
|
if (empty($tags)) return;
|
||||||
|
|
||||||
|
echo '<div class="tags">';
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$slug = strtolower(trim($tag));
|
||||||
|
echo '<a href="/tags/' . htmlspecialchars($slug) . '">#' . htmlspecialchars($slug) . '</a>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ class SelectPagination extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseIndex() {
|
get baseIndex() {
|
||||||
return this.getAttribute('data-base-index') || 0
|
return parseInt(this.getAttribute('data-base-index') || '0', 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
@ -17,10 +17,11 @@ class SelectPagination extends HTMLElement {
|
||||||
this.attachShadow({ mode: 'open' }).appendChild(document.createElement('slot'))
|
this.attachShadow({ mode: 'open' }).appendChild(document.createElement('slot'))
|
||||||
|
|
||||||
const uriSegments = window.location.pathname.split('/').filter(Boolean)
|
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 = this.querySelector('select')
|
||||||
this.control.value = pageNumber
|
this.control.value = pageNumber.toString()
|
||||||
this.control.addEventListener('change', (event) => {
|
this.control.addEventListener('change', (event) => {
|
||||||
pageNumber = parseInt(event.target.value)
|
pageNumber = parseInt(event.target.value)
|
||||||
const updatedUrlSegments = this.updateUrlSegments(uriSegments, pageNumber)
|
const updatedUrlSegments = this.updateUrlSegments(uriSegments, pageNumber)
|
||||||
|
@ -34,13 +35,16 @@ class SelectPagination extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrlSegments(segments, pageNumber) {
|
updateUrlSegments(segments, pageNumber) {
|
||||||
if (!isNaN(segments[segments.length - 1])) {
|
const lastIsPage = !isNaN(segments[segments.length - 1])
|
||||||
|
|
||||||
|
if (lastIsPage) {
|
||||||
segments[segments.length - 1] = pageNumber.toString()
|
segments[segments.length - 1] = pageNumber.toString()
|
||||||
} else {
|
} else {
|
||||||
segments.push(pageNumber.toString())
|
segments.push(pageNumber.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageNumber === parseInt(this.baseIndex)) segments.pop()
|
if (pageNumber === this.baseIndex) segments.pop()
|
||||||
|
|
||||||
return segments
|
return segments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,10 +257,7 @@ a {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
|
|
||||||
&:not(:has(+ h1, + h2, + h3)) {
|
|
||||||
margin-bottom: var(--spacing-base);
|
margin-bottom: var(--spacing-base);
|
||||||
}
|
|
||||||
|
|
||||||
&:is(:hover, :focus, :active) svg {
|
&:is(:hover, :focus, :active) svg {
|
||||||
transform: var(--transform-icon-default);
|
transform: var(--transform-icon-default);
|
||||||
|
@ -350,7 +347,13 @@ hr {
|
||||||
time {
|
time {
|
||||||
color: var(--gray-dark);
|
color: var(--gray-dark);
|
||||||
font-size: var(--font-size-sm);
|
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 {
|
article {
|
||||||
|
@ -366,6 +369,10 @@ article {
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
|
&:has(+ .tags) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
|
|
||||||
/* transforms */
|
/* transforms */
|
||||||
--transform-icon-default: rotate(0);
|
--transform-icon-default: rotate(0);
|
||||||
--transform-icon-tilt: rotate(7.5deg);
|
--transform-icon-tilt: rotate(10deg);
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
@media (prefers-reduced-motion) {
|
||||||
--transform-icon-tilt: var(--transform-icon-default);
|
--transform-icon-tilt: var(--transform-icon-default);
|
||||||
|
|
|
@ -22,11 +22,6 @@ dialog {
|
||||||
transition: color var(--transition-duration-default) var(--transition-ease-in-out),
|
transition: color var(--transition-duration-default) var(--transition-ease-in-out),
|
||||||
transform 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) {
|
&:is(:hover, :focus, :active) {
|
||||||
color: var(--link-color-hover);
|
color: var(--link-color-hover);
|
||||||
|
|
||||||
|
@ -77,6 +72,10 @@ dialog {
|
||||||
left: var(--sizing-full);
|
left: var(--sizing-full);
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
border-radius: var(--border-radius-full);
|
border-radius: var(--border-radius-full);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
--sizing-svg: var(--sizing-svg-base);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,10 +44,6 @@ button svg,
|
||||||
label svg {
|
label svg {
|
||||||
stroke: var(--section-color, var(--accent-color));
|
stroke: var(--section-color, var(--accent-color));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:is(:hover, :focus, :active) {
|
|
||||||
stroke: var(--accent-color-hover);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
|
|
36
src/data/tags.js
Normal file
36
src/data/tags.js
Normal file
|
@ -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;
|
||||||
|
}
|
7
src/includes/blocks/tags.liquid
Normal file
7
src/includes/blocks/tags.liquid
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% if tags %}
|
||||||
|
<div class="tags">
|
||||||
|
{%- for tag in tags -%}
|
||||||
|
<a href="/tags/{{ tag | downcase | url_encode }}">#{{ tag | downcase }}</a>
|
||||||
|
{%- endfor -%}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
109
src/includes/fetchers/tags.php.liquid
Normal file
109
src/includes/fetchers/tags.php.liquid
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
require __DIR__ . "/../vendor/autoload.php";
|
||||||
|
require __DIR__ . "/../server/utils/init.php";
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use voku\helper\HtmlMin;
|
||||||
|
|
||||||
|
$requestUri = $_SERVER["REQUEST_URI"];
|
||||||
|
$url = trim(parse_url($requestUri, PHP_URL_PATH), "/");
|
||||||
|
$postgrestUrl = $_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL");
|
||||||
|
$postgrestApiKey = $_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY");
|
||||||
|
|
||||||
|
if ($url === "tags") {
|
||||||
|
readfile("index.html");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^tags\/(.+?)(?:\/(\d+))?$/', $url, $matches)) {
|
||||||
|
echo file_get_contents(__DIR__ . "/../404/index.html");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($matches[2]) && (int)$matches[2] === 1) {
|
||||||
|
header("Location: /tags/{$matches[1]}", true, 301);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = strtolower(urldecode($matches[1]));
|
||||||
|
|
||||||
|
if (!preg_match('/^[\p{L}\p{N} _\-]+$/u', $tag)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid tag";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageParam = isset($matches[2]) ? (int)$matches[2] : 0;
|
||||||
|
$page = max($pageParam, 1);
|
||||||
|
$pageSize = 20;
|
||||||
|
$offset = ($page - 1) * $pageSize;
|
||||||
|
|
||||||
|
$cacheKey = "tag_{$tag}_{$page}";
|
||||||
|
$useRedis = false;
|
||||||
|
$tagged = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (extension_loaded('redis')) {
|
||||||
|
$redis = new Redis();
|
||||||
|
$redis->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");
|
||||||
|
|
||||||
|
?>
|
|
@ -16,8 +16,8 @@
|
||||||
<span class="client-side">
|
<span class="client-side">
|
||||||
{%- if item.notes -%}
|
{%- if item.notes -%}
|
||||||
{% assign notes = item.notes | prepend: "### Notes\n" | markdown %}
|
{% assign notes = item.notes | prepend: "### Notes\n" | markdown %}
|
||||||
• {% render "blocks/dialog.liquid",
|
{% render "blocks/dialog.liquid",
|
||||||
label:"Notes",
|
icon:"info-circle",
|
||||||
content:notes,
|
content:notes,
|
||||||
id:item.content_date
|
id:item.content_date
|
||||||
%}
|
%}
|
||||||
|
@ -54,6 +54,9 @@
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</h3>
|
</h3>
|
||||||
|
{% render "blocks/tags.liquid",
|
||||||
|
18 tags:item.tags
|
||||||
|
19 %}
|
||||||
</article>
|
</article>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -5,3 +5,11 @@
|
||||||
<meta property="og:image" content="<?= htmlspecialchars("{{ globals.cdn_url }}" . ($ogImage ?? '{{ ogImage }}'), ENT_QUOTES, 'UTF-8') ?>" />
|
<meta property="og:image" content="<?= htmlspecialchars("{{ globals.cdn_url }}" . ($ogImage ?? '{{ ogImage }}'), ENT_QUOTES, 'UTF-8') ?>" />
|
||||||
<meta property="og:url" content="<?= htmlspecialchars($fullUrl ?? '{{ fullUrl }}', ENT_QUOTES, 'UTF-8') ?>" />
|
<meta property="og:url" content="<?= htmlspecialchars($fullUrl ?? '{{ fullUrl }}', ENT_QUOTES, 'UTF-8') ?>" />
|
||||||
<link rel="canonical" href="<?= htmlspecialchars($fullUrl ?? '{{ fullUrl }}', ENT_QUOTES, 'UTF-8') ?>" />
|
<link rel="canonical" href="<?= htmlspecialchars($fullUrl ?? '{{ fullUrl }}', ENT_QUOTES, 'UTF-8') ?>" />
|
||||||
|
<?php if (!empty($pagination)): ?>
|
||||||
|
<?php if ($pagination['href']['next']): ?>
|
||||||
|
<link rel="next" href="<?= $pagination['href']['next'] ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($pagination['href']['previous']): ?>
|
||||||
|
<link rel="prev" href="<?= $pagination['href']['previous'] ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
{% render "fetchers/movie.php.liquid" %}
|
{% render "fetchers/movie.php.liquid" %}
|
||||||
{%- when 'show' -%}
|
{%- when 'show' -%}
|
||||||
{% render "fetchers/show.php.liquid" %}
|
{% render "fetchers/show.php.liquid" %}
|
||||||
|
{%- when 'tags' -%}
|
||||||
|
{% render "fetchers/tags.php.liquid" %}
|
||||||
{%- when 'blog' -%}
|
{%- when 'blog' -%}
|
||||||
{%- assign pageTitle = post.title -%}
|
{%- assign pageTitle = post.title -%}
|
||||||
{%- assign pageDescription = post.description -%}
|
{%- assign pageDescription = post.description -%}
|
||||||
|
|
|
@ -25,9 +25,7 @@ ErrorDocument 404 /404/index.html
|
||||||
ErrorDocument 429 /429/index.html
|
ErrorDocument 429 /429/index.html
|
||||||
ErrorDocument 500 /500/index.html
|
ErrorDocument 500 /500/index.html
|
||||||
|
|
||||||
# media routing
|
# dynamic page routing
|
||||||
|
|
||||||
# media routing
|
|
||||||
|
|
||||||
## artists
|
## artists
|
||||||
RewriteRule ^music/artists/([^/]+)/?$ music/artists/index.php [L]
|
RewriteRule ^music/artists/([^/]+)/?$ music/artists/index.php [L]
|
||||||
|
@ -46,6 +44,9 @@ RewriteRule ^watching/shows/([^/]+)/?$ watching/shows/index.php [L]
|
||||||
## genres
|
## genres
|
||||||
RewriteRule ^music/genres/([^/]+)/?$ music/genres/index.php [L]
|
RewriteRule ^music/genres/([^/]+)/?$ music/genres/index.php [L]
|
||||||
|
|
||||||
|
## tags
|
||||||
|
RewriteRule ^tags/([^/]+)(?:/([0-9]+))?/?$ tags/index.php [L]
|
||||||
|
|
||||||
{% for redirect in redirects -%}
|
{% for redirect in redirects -%}
|
||||||
Redirect {{ redirect.status_code | default: "301" }} {{ redirect.source_url }} {{ redirect.destination_url }}
|
Redirect {{ redirect.status_code | default: "301" }} {{ redirect.source_url }} {{ redirect.destination_url }}
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
|
|
|
@ -23,7 +23,7 @@ schema: artist
|
||||||
height="200"
|
height="200"
|
||||||
/>
|
/>
|
||||||
<div class="media-meta">
|
<div class="media-meta">
|
||||||
<h2><?= htmlspecialchars($artist["name"]) ?></h2>
|
<h2 class="page-title"><?= htmlspecialchars($artist["name"]) ?></h2>
|
||||||
<span class="sub-meta country">{% tablericon "map-pin" %} <?= htmlspecialchars(
|
<span class="sub-meta country">{% tablericon "map-pin" %} <?= htmlspecialchars(
|
||||||
parseCountryField($artist["country"])
|
parseCountryField($artist["country"])
|
||||||
) ?></span>
|
) ?></span>
|
||||||
|
|
|
@ -24,7 +24,10 @@ schema: book
|
||||||
height="307"
|
height="307"
|
||||||
/>
|
/>
|
||||||
<div class="media-meta">
|
<div class="media-meta">
|
||||||
<h2><?= htmlspecialchars($book["title"]) ?></h2>
|
<h2 class="page-title"><?= htmlspecialchars($book["title"]) ?></h2>
|
||||||
|
<?php if (!empty($book["tags"])): ?>
|
||||||
|
<?php renderTags($book["tags"] ?? []); ?>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!empty($book["rating"])): ?>
|
<?php if (!empty($book["rating"])): ?>
|
||||||
<span><?= htmlspecialchars($book["rating"]) ?></span>
|
<span><?= htmlspecialchars($book["rating"]) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
|
@ -4,7 +4,7 @@ type: dynamic
|
||||||
schema: genre
|
schema: genre
|
||||||
---
|
---
|
||||||
<a class="back-link" href="/music" title="Go back to the music index page">{% tablericon "arrow-left" %} Back to music</a>
|
<a class="back-link" href="/music" title="Go back to the music index page">{% tablericon "arrow-left" %} Back to music</a>
|
||||||
<h2><?= htmlspecialchars($genre["emoji"]) ?> <?= htmlspecialchars($genre["name"]) ?></h2>
|
<h2 class="page-title"><?= htmlspecialchars($genre["emoji"]) ?> <?= htmlspecialchars($genre["name"]) ?></h2>
|
||||||
<article class="genre-focus">
|
<article class="genre-focus">
|
||||||
<?php $artistCount = count($genre["artists"]); ?>
|
<?php $artistCount = count($genre["artists"]); ?>
|
||||||
<?php if ($artistCount > 0): ?>
|
<?php if ($artistCount > 0): ?>
|
||||||
|
|
|
@ -22,7 +22,10 @@ schema: movie
|
||||||
height="180"
|
height="180"
|
||||||
/>
|
/>
|
||||||
<div class="media-meta">
|
<div class="media-meta">
|
||||||
<h2><?= htmlspecialchars($movie["title"]) ?> (<?= htmlspecialchars($movie["year"]) ?>)</h2>
|
<h2 class="page-title"><?= htmlspecialchars($movie["title"]) ?> (<?= htmlspecialchars($movie["year"]) ?>)</h2>
|
||||||
|
<?php if (!empty($movie["tags"])): ?>
|
||||||
|
<?php renderTags($movie["tags"] ?? []); ?>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!empty($movie["rating"])): ?>
|
<?php if (!empty($movie["rating"])): ?>
|
||||||
<span><?= htmlspecialchars($movie["rating"]) ?></span>
|
<span><?= htmlspecialchars($movie["rating"]) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
|
@ -22,7 +22,10 @@ schema: show
|
||||||
height="180"
|
height="180"
|
||||||
/>
|
/>
|
||||||
<div class="media-meta">
|
<div class="media-meta">
|
||||||
<h2><?= htmlspecialchars($show["title"]) ?> (<?= htmlspecialchars($show["year"]) ?>)</h2>
|
<h2 class="page-title"><?= htmlspecialchars($show["title"]) ?> (<?= htmlspecialchars($show["year"]) ?>)</h2>
|
||||||
|
<?php if (!empty($show["tags"])): ?>
|
||||||
|
<?php renderTags($show["tags"] ?? []); ?>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!empty($show["favorite"])): ?>
|
<?php if (!empty($show["favorite"])): ?>
|
||||||
<span class="sub-meta favorite">{% tablericon "heart" %} This is one of my favorite shows!</span>
|
<span class="sub-meta favorite">{% tablericon "heart" %} This is one of my favorite shows!</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
50
src/pages/dynamic/tags.php.liquid
Normal file
50
src/pages/dynamic/tags.php.liquid
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
permalink: /tags/index.php
|
||||||
|
type: dynamic
|
||||||
|
schema: tags
|
||||||
|
---
|
||||||
|
<a class="back-link" href="/tags" title="Go back to the tags index page">{% tablericon "arrow-left" %} Back to tags</a>
|
||||||
|
<h2 class="page-title">#<?= htmlspecialchars($tag) ?></h2>
|
||||||
|
<p><mark><?= number_format($totalCount) ?> item<?= $totalCount === 1 ? '' : 's' ?></mark> items tagged with <mark>#<?= htmlspecialchars($tag) ?></mark>. <a class="search" href="/search">You can search my site as well</a>.</p>
|
||||||
|
<hr />
|
||||||
|
<?php foreach ($tagged as $item): ?>
|
||||||
|
<article class="<?= $item['type'] ?>">
|
||||||
|
<aside>
|
||||||
|
<?php if (!empty($item['featured'])): ?>
|
||||||
|
<?= getTablerIcon('star') ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<time datetime="<?= date('F j, Y', strtotime($item['content_date'])) ?>">
|
||||||
|
<?= date('F j, Y', strtotime($item['content_date'])) ?>
|
||||||
|
</time>
|
||||||
|
• <?= $item['label'] ?>
|
||||||
|
</aside>
|
||||||
|
<h3>
|
||||||
|
<a href="<?= htmlspecialchars($item['url']) ?>"><?= htmlspecialchars($item['title']) ?><?php if (!empty($item['rating'])): ?> (<?= $item['rating'] ?>)<?php endif; ?></a>
|
||||||
|
<?php if (!empty($item['author'])): ?>
|
||||||
|
 via 
|
||||||
|
<?php if (!empty($item['author']['url'])): ?>
|
||||||
|
<a href="<?= $item['author']['url'] ?>">
|
||||||
|
<?= $item['author']['name'] ?>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= $item['author']['name'] ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</h3>
|
||||||
|
<?php if (!empty($item["tags"])): ?>
|
||||||
|
<?php renderTags($item["tags"] ?? []); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php renderPaginator($pagination, $totalPages); ?>
|
||||||
|
<?php
|
||||||
|
$html = ob_get_clean();
|
||||||
|
$htmlMin = new HtmlMin();
|
||||||
|
$htmlMin->doOptimizeAttributes(true);
|
||||||
|
$htmlMin->doRemoveComments(true);
|
||||||
|
$htmlMin->doSumUpWhitespace(true);
|
||||||
|
$htmlMin->doRemoveWhitespaceAroundTags(true);
|
||||||
|
$htmlMin->doOptimizeViaHtmlDomParser(true);
|
||||||
|
echo $htmlMin->minify($html);
|
||||||
|
?>
|
|
@ -22,6 +22,9 @@ permalink: "/links/{% if pagination.pageNumber > 0 %}{{ pagination.pageNumber }}
|
||||||
<article>
|
<article>
|
||||||
<a href="{{ link.link }}" title="{{ link.title | escape }}"><strong>{{ link.title }}</strong></a>
|
<a href="{{ link.link }}" title="{{ link.title | escape }}"><strong>{{ link.title }}</strong></a>
|
||||||
{% if link.author %} via <a href="{{ link.author.url }}">{{ link.author.name }}</a>{% endif %}
|
{% if link.author %} via <a href="{{ link.author.url }}">{{ link.author.name }}</a>{% endif %}
|
||||||
|
{% render "blocks/tags.liquid",
|
||||||
|
tags:link.tags
|
||||||
|
%}
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -26,6 +26,9 @@ permalink: "/posts/{% if pagination.pageNumber > 0 %}{{ pagination.pageNumber }}
|
||||||
<h3>
|
<h3>
|
||||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
{% render "blocks/tags.liquid",
|
||||||
|
tags:post.tags
|
||||||
|
%}
|
||||||
<p>{{ post.description | markdown }}</p>
|
<p>{{ post.description | markdown }}</p>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -13,9 +13,10 @@ schema: blog
|
||||||
{{ post.date | date: "%B %e, %Y" }}
|
{{ post.date | date: "%B %e, %Y" }}
|
||||||
</time>
|
</time>
|
||||||
</aside>
|
</aside>
|
||||||
<h3>
|
<h3>{{ post.title }}</h3>
|
||||||
{{ post.title }}
|
{% render "blocks/tags.liquid",
|
||||||
</h3>
|
tags:post.tags
|
||||||
|
%}
|
||||||
<div>
|
<div>
|
||||||
{% render "blocks/banners/old-post.liquid",
|
{% render "blocks/banners/old-post.liquid",
|
||||||
isOldPost:post.old_post
|
isOldPost:post.old_post
|
||||||
|
|
26
src/pages/sections/tags.html
Normal file
26
src/pages/sections/tags.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
title: Tags
|
||||||
|
description: All of the tags I've applied to content on this site — posts, links, books, movies and shows.
|
||||||
|
permalink: /tags/index.html
|
||||||
|
---
|
||||||
|
<h2 class="page-title">Tags</h2>
|
||||||
|
<p>All of the tags I've applied to content on this site — posts, links, books, movies and shows. <a class="search" href="/search">You can search my site as well</a>.</p>
|
||||||
|
<hr />
|
||||||
|
<nav aria-label="Tag navigation">
|
||||||
|
{% for group in tags %}
|
||||||
|
<a href="#{{ group.letter }}">{{ group.letter }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
{% for group in tags %}
|
||||||
|
<section id="{{ group.letter }}">
|
||||||
|
<h3>{{ group.letter }}</h3>
|
||||||
|
<ul class="tag-list">
|
||||||
|
{% for tag in group.tags %}
|
||||||
|
<li>
|
||||||
|
<a href="/tags/{{ tag.tag | downcase }}">#{{ tag.tag }}</a>
|
||||||
|
<span class="count">({{ tag.uses }})</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endfor %}
|
|
@ -25,11 +25,9 @@ excludeFromSitemap: true
|
||||||
height="480"
|
height="480"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center">
|
<h2 style="text-align:center">404</h2>
|
||||||
<h2>404</h2>
|
<p style="text-align:center">What kind of idiots do you have working here?</p>
|
||||||
<p>What kind of idiots do you have working here?</p>
|
<p style="text-align:center"><a href="/">Hurry up and skip out on the room service bill!</a></p>
|
||||||
<p><a href="/">Hurry up and skip out on the room service bill!</a></p>
|
|
||||||
</div>
|
|
||||||
<script>
|
<script>
|
||||||
const track404 = () => {
|
const track404 = () => {
|
||||||
const referrer = document.referrer || "Unknown referrer";
|
const referrer = document.referrer || "Unknown referrer";
|
||||||
|
|
|
@ -5,25 +5,10 @@ description: Search through posts and other content on my site.
|
||||||
---
|
---
|
||||||
|
|
||||||
<h2 class="page-title">Search</h2>
|
<h2 class="page-title">Search</h2>
|
||||||
<p>
|
<p>You can find <a href="/posts">posts</a>, <a href="/links">links</a>, <a href="/music/#artists">artists</a>, genres, <a href="/watching#movies">movies</a>, <a href="/watching#tv">shows</a> and <a href="/books">books</a> via the field below (though it only surfaces movies and shows I've watched and books I've written something about). <a href="/tags">You can also browse my tags list</a>.</p>
|
||||||
You can find <a href="/posts">posts</a>, <a href="/links">links</a>,
|
|
||||||
<a href="/music/#artists">artists</a>, genres,
|
|
||||||
<a href="/watching#movies">movies</a>, <a href="/watching#tv">shows</a> and
|
|
||||||
<a href="/books">books</a> via the field below (though it only surfaces movies
|
|
||||||
and shows I've watched and books I've written something about).
|
|
||||||
</p>
|
|
||||||
<noscript>
|
<noscript>
|
||||||
<p>
|
<p><mark>If you're seeing this it means that you've (quite likely) disabled JavaScript (that's a totally valid choice!).</mark> You can search for anything on my site using the form below, but your query will be routed through <a href="https://duckduckgo.com">DuckDuckGo</a>.</p>
|
||||||
<mark
|
<p><mark>Type something in and hit enter.</mark></p>
|
||||||
>If you're seeing this it means that you've (quite likely) disabled
|
|
||||||
JavaScript (that's a totally valid choice!).</mark>
|
|
||||||
You can search for anything on my site using the form below, but your query
|
|
||||||
will be routed through
|
|
||||||
<a href="https://duckduckgo.com">DuckDuckGo</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<mark>Type something in and hit enter.</mark>
|
|
||||||
</p>
|
|
||||||
</noscript>
|
</noscript>
|
||||||
<form class="search__form" action="https://duckduckgo.com" method="get">
|
<form class="search__form" action="https://duckduckgo.com" method="get">
|
||||||
<input
|
<input
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue