feat: article tags link to articles with the same tag

This commit is contained in:
Troy 2025-03-11 20:56:54 +00:00
parent 1b1bbae3ce
commit aa26e7cd55
Signed by: troy
GPG key ID: DFC06C02ED3B4711
10 changed files with 309 additions and 363 deletions

View file

@ -2,7 +2,7 @@
import Layout from "@layouts/Layout.astro";
import Prose from "@components/Prose.astro";
import FormattedDate from "@components/FormattedDate.astro";
import { readingTime } from "@lib/utils";
import { readingTime, createSlug } from "@lib/utils";
import { Icon } from "astro-icon/components";
import RelatedArticles from "@components/RelatedArticles.astro";
@ -76,6 +76,21 @@ const listFormatter = new Intl.ListFormat("en-GB", {
</div>
) : null
}
{
article.data.tags ? (
<div class="flex flex-wrap items-center gap-2">
<Icon name="mdi:tag" />
{article.data.tags.map((tag: any) => (
<a
href={`/tags/${createSlug(tag)}`}
class="underline hover:no-underline"
>
{tag}
</a>
))}
</div>
) : null
}
</div>
<div
class="animate-reveal mx-auto max-w-full opacity-0 [animation-delay:0.2s]"

View file

@ -4,7 +4,7 @@ description: "What would you do if you awoke to find your breakfast entirely bea
date: 2026-01-01
updated: 2026-01-01
image: { url: "capsule.avif", alt: "MUST FIND BEANS Title Logo" }
tags: ["godot", "blender"]
tags: ["godot", "blender", "test"]
categories: ["personal"]
includeHero: true
draft: true

View file

@ -49,3 +49,19 @@ export function dateRange(startDate: Date, endDate?: Date | string): string {
return `${startMonth} ${startYear} - ${endMonth} ${endYear}`;
}
}
export function createSlug(title: string): string {
return (
title
// remove leading & trailing whitespace
.trim()
// remove special characters
.replace(/[^A-Za-z0-9 ]/g, "")
// replace spaces
.replace(/\s+/g, "-")
// remove leading & trailing separtors
.replace(/^-+|-+$/g, "")
// output lowercase
.toLowerCase()
);
}

View file

@ -197,9 +197,13 @@ const sortedEducation = [...education].sort((a, b) => a.id - b.id);
<h3
class="inline-flex items-center justify-center gap-x-1 leading-none font-semibold"
>
<Link class="hover:underline" href="/projects/camouflage-store"
>Camouflage Store</Link
<Link
href="/projects/camouflage-store"
class="text-secondary inline-flex items-center gap-1 hover:underline"
>
<span class="mr-1 h-1 w-1 rounded-full bg-green-500"></span>
Camouflage Store
</Link>
</h3><div class="text-tertiary text-sm tabular-nums">
2020 - Current
</div>

View file

@ -3,6 +3,8 @@ import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) => `
User-agent: *
Disallow: /cv
Disallow: /tags
Disallow: /tags/*
Allow: /
Sitemap: ${sitemapURL.href}

View file

@ -0,0 +1,70 @@
---
import { getCollection } from "astro:content";
import { SITE } from "@consts";
import Layout from "@layouts/Layout.astro";
import ShowcasePost from "@components/ShowcasePost.astro";
import { createSlug } from "@lib/utils";
export async function getStaticPaths() {
const allBlogPosts = (await getCollection("posts")).filter(
(post) => !post.data.draft,
);
const allProjects = (await getCollection("projects")).filter(
(post) => !post.data.draft,
);
const blogTags = allBlogPosts.flatMap((post) => post.data.tags);
const projectTags = allProjects.flatMap((project) => project.data.tags);
const allTags = new Set([...blogTags, ...projectTags]);
return Array.from(allTags).map((tag) => {
const slug = createSlug(tag);
return {
params: { tag: slug },
props: { originalTag: tag },
};
});
}
const { originalTag } = Astro.props;
const allBlogPosts = await getCollection("posts");
const allProjects = await getCollection("projects");
const blogPostsWithTag = allBlogPosts.filter((post) =>
post.data.tags.includes(originalTag),
);
const projectsWithTag = allProjects.filter((project) =>
project.data.tags.includes(originalTag),
);
const allArticlesWithTag = [...blogPostsWithTag, ...projectsWithTag]
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
---
<Layout title={SITE.TITLE} description={`Articles tagged with ${originalTag}.`}>
<h1 class="animate-reveal text-3xl font-semibold break-words opacity-0">
<span class="capitalize">{originalTag}</span> is tagged on {
allArticlesWithTag.length
}
{
allArticlesWithTag.length > 1 ? (
<span>articles</span>
) : (
<span>article</span>
)
}
</h1>
<ul
class="animate-reveal grid grid-cols-1 gap-6 opacity-0 [animation-delay:0.1s]"
>
{allArticlesWithTag.map((article) => <ShowcasePost collection={article} />)}
</ul>
<a
href="/tags"
class="animate-reveal w-fit underline opacity-0 [animation-delay:0.2s] hover:no-underline"
>See all tags</a
>
</Layout>

View file

@ -0,0 +1,50 @@
---
import { getCollection } from "astro:content";
import { SITE } from "@consts";
import Layout from "@layouts/Layout.astro";
import { createSlug } from "@lib/utils";
function freqSort(items: Array<string>) {
var cnts = items.reduce(function (obj: any, val) {
obj[val] = (obj[val] || 0) + 1;
return obj;
}, {});
var sorted = Object.keys(cnts).sort(function (a, b) {
return cnts[b] - cnts[a];
});
return sorted;
}
const allBlogPosts = (await getCollection("posts")).filter(
(post) => !post.data.draft,
);
const allProjects = (await getCollection("projects")).filter(
(post) => !post.data.draft,
);
const blogTags = allBlogPosts.flatMap((post) => post.data.tags);
const projectTags = allProjects.flatMap((project) => project.data.tags);
const allTags = blogTags.concat(projectTags);
const allTagsSorted = freqSort(allTags);
---
<Layout title={SITE.TITLE} description="All tags found on articles.">
<h1 class="animate-reveal text-3xl font-semibold break-words opacity-0">
Tags
</h1>
<ol
class="text-tertiary animate-reveal flex flex-wrap items-center gap-2 opacity-0 [animation-delay:0.1s]"
>
{
allTagsSorted.map((tag: any) => (
<a
href={`/tags/${createSlug(tag)}`}
class="underline hover:no-underline"
>
{tag}
</a>
))
}
</ol>
</Layout>