Added tag pages

This commit is contained in:
HangerThem 2026-08-04 08:06:05 +02:00
parent f3f351bbbc
commit 9b5fee37e2
9 changed files with 200 additions and 6 deletions

View file

@ -48,7 +48,7 @@ export default async function CategoryPage({ params }: CategoryPageProps) {
} }
return ( return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 w-full"> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16 w-full">
<h1>Category: {category}</h1> <h1>Category: {category}</h1>
<ul> <ul>
{posts.map((post) => ( {posts.map((post) => (

View file

@ -33,7 +33,7 @@ export default function CategoriesPage() {
const categoryNames = Object.keys(categories) const categoryNames = Object.keys(categories)
return ( return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 w-full"> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16 w-full">
<div className="text-sm text-fg-dim mb-2">~/categories</div> <div className="text-sm text-fg-dim mb-2">~/categories</div>
<ul className="border-l border-neutral-800 ml-1"> <ul className="border-l border-neutral-800 ml-1">

View file

@ -11,7 +11,7 @@ export default function BlogIndex() {
} }
return ( return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-16 lg:py-16"> <main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16">
<section> <section>
{posts.length === 0 ? ( {posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim"> <div className="rounded-xl border border-border p-6 text-fg-dim">

View file

@ -0,0 +1,62 @@
import { metadata } from '@/app/metadata'
import { getAllTags, getPostsByTag, getTagBySlug } from '@/lib/tags'
import type { Metadata } from 'next'
import Link from 'next/link'
type TagPageProps = {
params: Promise<{
tagSlug: string
}>
}
export async function generateStaticParams() {
const tags = getAllTags()
return Object.keys(tags).map((tag) => ({
tagSlug: tag,
}))
}
export async function generateMetadata({ params }: TagPageProps): Promise<Metadata> {
const { tagSlug } = await params
const tag = getTagBySlug(tagSlug)
return {
title: `${tag} | ${metadata.title!.toString()}`,
description: 'List of posts in this tag',
openGraph: {
type: 'website',
title: `${tag} | ${metadata.title!.toString()}`,
description: 'List of posts in this tag',
url: process.env.SITE_URL + `/tags/${tagSlug}`,
siteName: metadata.title!.toString(),
},
}
}
export default async function TagPage({ params }: TagPageProps) {
const { tagSlug } = await params
const posts = getPostsByTag(tagSlug)
const tag = getTagBySlug(tagSlug)
if (!tag) {
return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 w-full">
<h1>Tag not found</h1>
<Link href="/tags">Back to tags</Link>
</main>
)
}
return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16 w-full">
<h1>Tag: {tag}</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</main>
)
}

View file

@ -0,0 +1,83 @@
import { getAllTags } from '@/lib/tags'
import { getAllPostsMeta } from '@/lib/posts'
import { slugify } from '@/utils/slug'
import type { Metadata } from 'next'
import Link from 'next/link'
import { metadata } from '@/app/metadata'
import { dateFormatter } from '@/utils/time'
export async function generateStaticParams() {
const tags = getAllTags()
return Object.keys(tags).map((tag) => ({
tagSlug: tag,
}))
}
export async function generateMetadata(): Promise<Metadata> {
return {
title: `Tags | ${metadata.title!.toString()}`,
description: 'List of tags',
openGraph: {
type: 'website',
title: `Tags | ${metadata.title!.toString()}`,
description: 'List of tags',
url: process.env.SITE_URL + '/tags',
siteName: metadata.title!.toString(),
},
}
}
export default function TagsPage() {
const tags = getAllTags()
const posts = getAllPostsMeta()
const tagNames = Object.keys(tags)
return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16 w-full">
<div className="text-sm text-fg-dim mb-2">~/tags</div>
<ul className="border-l border-neutral-800 ml-1">
{tagNames.map((tag) => {
const tagPosts = posts.filter((post) => post.tags && post.tags.includes(tag))
return (
<li key={tag} className="pl-5">
<Link
href={`/tags/${slugify(tag)}`}
className="group flex items-baseline gap-2 py-2 px-2 -ml-2 hover:bg-accent/10 transition-colors"
>
<span className="font-bold group-hover:text-accent transition-colors capitalize">
{tag}
</span>
<span className="font-mono text-xs text-neutral-600">({tagPosts.length})</span>
</Link>
<ul className="border-l border-neutral-800 ml-3 mb-3">
{tagPosts.map((post) => (
<li key={post.slug} className="pl-5">
<Link
href={`/posts/${post.slug}`}
className="group flex items-baseline justify-between gap-3 py-1.5 px-2 -ml-2 border-l-2 border-transparent hover:bg-accent/10 hover:border-accent transition-colors"
>
<span className="group-hover:text-accent transition-colors truncate">
{post.title}
</span>
{post.date && (
<time
dateTime={new Date(post.date).toISOString()}
className="text-sm whitespace-nowrap"
>
{dateFormatter.format(new Date(post.date))}
</time>
)}
</Link>
</li>
))}
</ul>
</li>
)
})}
</ul>
</main>
)
}

View file

@ -44,7 +44,7 @@ export default async function Post({ params }: { params: Promise<{ slug: string
return ( return (
<main className="flex w-full"> <main className="flex w-full">
<article className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 lg:py-16 min-w-0"> <article className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16 min-w-0">
{meta.category && ( {meta.category && (
<Link <Link
href={`/categories/${slugify(meta.category)}`} href={`/categories/${slugify(meta.category)}`}

View file

@ -59,7 +59,10 @@ export async function Sidebar() {
{gitCommitHash && gitOriginUrl && ( {gitCommitHash && gitOriginUrl && (
<div className="text-xs text-fg-dim mt-2 mx-auto"> <div className="text-xs text-fg-dim mt-2 mx-auto">
<span>Commit: </span> <span>Commit: </span>
<ExternalLink href={gitOriginUrl + '/commit/' + gitCommitHash}> <ExternalLink
href={gitOriginUrl + '/commit/' + gitCommitHash}
className="hover:underline"
>
{gitCommitHash.slice(0, 7)} {gitCommitHash.slice(0, 7)}
</ExternalLink> </ExternalLink>
</div> </div>

View file

@ -4,7 +4,7 @@ import Link from 'next/link'
export function RecentlyUpdatedPosts() { export function RecentlyUpdatedPosts() {
const posts = getAllPostsMeta() const posts = getAllPostsMeta()
return ( return (
<aside className="w-80 xl:w-100 hidden lg:block"> <aside className="w-80 2xl:w-100 hidden lg:block">
<div className="sticky top-12 border-l border-border h-fit pl-4 py-4"> <div className="sticky top-12 border-l border-border h-fit pl-4 py-4">
<h2 className="text-xl font-bold mb-4">Recently updated</h2> <h2 className="text-xl font-bold mb-4">Recently updated</h2>
<ul className="flex flex-col gap-2 ml-1"> <ul className="flex flex-col gap-2 ml-1">

46
lib/tags.ts Normal file
View file

@ -0,0 +1,46 @@
import { findSlugInArray } from '@/utils/slug'
import { getAllPostsMeta } from '@/lib/posts'
/**
* Retrieves all unique tags from the posts and counts the number of posts for each tag.
* @returns An object where keys are tag names and values are the count of posts for that tag.
*/
export function getAllTags(): Record<string, number> {
const posts = getAllPostsMeta()
const tags = {} as Record<string, number>
posts.forEach((post) => {
const tagsArray = post.tags || []
tagsArray.forEach((tag) => {
tags[tag] = (tags[tag] || 0) + 1
})
})
return tags
}
/**
* Finds a tag by its slug.
* @param tagSlug - The slug of the tag to find.
* @returns The original tag name if found, otherwise null.
*/
export function getTagBySlug(tagSlug: string): string | null {
const tags = getAllTags()
return findSlugInArray(tagSlug, Object.keys(tags))
}
/**
* Retrieves all posts that belong to a specific tag.
* @param tagSlug - The slug of the tag to filter posts by.
* @returns An array of posts that belong to the specified tag.
*/
export function getPostsByTag(tagSlug: string) {
const tags = getAllTags()
const tag = findSlugInArray(tagSlug, Object.keys(tags))
if (!tag) {
return []
}
return getAllPostsMeta().filter((post) => post.tags?.includes(tag))
}