Added tag pages
This commit is contained in:
parent
f3f351bbbc
commit
9b5fee37e2
9 changed files with 200 additions and 6 deletions
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
62
app/(withRecent)/tags/[tagSlug]/page.tsx
Normal file
62
app/(withRecent)/tags/[tagSlug]/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
83
app/(withRecent)/tags/page.tsx
Normal file
83
app/(withRecent)/tags/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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)}`}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
46
lib/tags.ts
Normal 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))
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue