feat: article tags link to articles with the same tag
This commit is contained in:
parent
1b1bbae3ce
commit
aa26e7cd55
10 changed files with 309 additions and 363 deletions
|
@ -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]"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
70
src/pages/tags/[tag].astro
Normal file
70
src/pages/tags/[tag].astro
Normal 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>
|
50
src/pages/tags/index.astro
Normal file
50
src/pages/tags/index.astro
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue