diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a89e1..cc6a5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SocialIcon component - Share post component - Lint staged files +- Tag generation and routing ### Changed diff --git a/README.md b/README.md index 37d1b37..f94105f 100644 --- a/README.md +++ b/README.md @@ -174,13 +174,15 @@ Note: DO NOT overdo it. You can easily make images look bad with lossy compressi - [x] Back to top button - [x] Social icons component - [x] Social sharing buttons -- [ ] Tags, categories +- [x] Tags - [ ] newsletter integration (form, api route, keys, welcome page, previous issues) - [ ] Other analytics providers (fathom, simplelytics, plausible, etc) - [ ] Post series page - [ ] prev/next post links - [ ] related posts - [ ] Layouts/templates system +- [ ] Notion data source +- [ ] Sanity data source - [ ] hero title and subtitle text HTML support(?) - [ ] Design improvements (whitespace, layout, etc.) - [ ] error, and loading pages diff --git a/app/(site)/page.tsx b/app/(site)/page.tsx index bef27be..e00680b 100644 --- a/app/(site)/page.tsx +++ b/app/(site)/page.tsx @@ -1,7 +1,8 @@ import Image from "next/image"; import Link from "next/link"; +import { notFound } from "next/navigation"; import { allPages, allPosts } from "@/.contentlayer/generated"; -import { compareDesc, format, parseISO } from "date-fns"; +import { compareDesc } from "date-fns"; import { ArrowRight } from "lucide-react"; import { defaultAuthor } from "@/lib/metadata"; @@ -16,13 +17,13 @@ import { Mdx } from "@/components/mdx-components"; import PostPreview from "@/components/post-preview"; async function getAboutPage() { - const page = allPages.find((page) => page.slug === "about"); + const aboutPage = allPages.find((page) => page.slug === "about"); - if (!page) { + if (!aboutPage) { null; } - return page; + return aboutPage; } export default async function Home() { diff --git a/app/(site)/posts/[slug]/page.tsx b/app/(site)/posts/[slug]/page.tsx index 1a54640..a90f2a1 100644 --- a/app/(site)/posts/[slug]/page.tsx +++ b/app/(site)/posts/[slug]/page.tsx @@ -161,9 +161,11 @@ export default async function PostPage({ params }: PostProps) {
{post.tags && ( diff --git a/app/(site)/posts/page.tsx b/app/(site)/posts/page.tsx index 258bc83..1c8dabe 100644 --- a/app/(site)/posts/page.tsx +++ b/app/(site)/posts/page.tsx @@ -18,6 +18,7 @@ export default function Blog() { .sort((a, b) => compareDesc(new Date(a.lastUpdatedDate || a.publishedDate), new Date(b.lastUpdatedDate || b.publishedDate)) ); + return (
diff --git a/app/(site)/tags/[slug]/page.tsx b/app/(site)/tags/[slug]/page.tsx new file mode 100644 index 0000000..20f957a --- /dev/null +++ b/app/(site)/tags/[slug]/page.tsx @@ -0,0 +1,55 @@ +import { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { allPosts, Post } from "@/.contentlayer/generated"; +import { compareDesc } from "date-fns"; + +import PostPreview from "@/components/post-preview"; + +// Get sorted articles from the contentlayer +async function getSortedArticles(): Promise { + let articles = await allPosts; + + articles = articles.filter((article: Post) => article.status === "published"); + + return articles.sort((a: Post, b: Post) => { + if (a.publishedDate && b.publishedDate) { + return new Date(b.publishedDate).getTime() - new Date(a.publishedDate).getTime(); + } + return 0; + }); +} + +// Dynamic metadata for the page +export async function generateMetadata({ params }: { params: { slug: string } }): Promise { + return { + title: `All posts in ${params.slug}`, + description: `All posts in ${params.slug}`, + }; +} + +export default async function TagPage({ params }: { params: { slug: string } }) { + const tag = params.slug; + + const posts = allPosts + .filter((post) => post.status === "published") + .filter((post) => post.tags?.includes(tag)) + .sort((a, b) => compareDesc(new Date(a.publishedDate), new Date(b.publishedDate))); + + if (!posts) { + notFound(); + } + + return ( +
+
+

All posts in {tag}

+
+
+ {posts.map((post) => ( + + ))} +
+
+
+ ); +} diff --git a/app/(site)/tags/page.tsx b/app/(site)/tags/page.tsx new file mode 100644 index 0000000..e63a00d --- /dev/null +++ b/app/(site)/tags/page.tsx @@ -0,0 +1,54 @@ +import { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { allPosts } from "@/.contentlayer/generated"; + +import siteMetadata from "@/lib/metadata"; +import { getTagsWithCount } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; + +export async function generateMetadata(): Promise { + return { + title: "Tags", + description: `All tags in ${siteMetadata.title}`, + }; +} + +export default function TagsPage() { + const posts = allPosts.filter((post) => post.status === "published"); + + const tags = getTagsWithCount(posts); + + if (!tags || Object.keys(tags).length === 0) { + notFound(); + } + + return ( +
+
+

All tags

+
+
+
    + {Object.keys(tags).map((tag) => { + return ( +
  • + + + + {tag} · ({tags[tag]}) + + + +
  • + ); + })} +
+
+
+
+ ); +} diff --git a/components/cta.tsx b/components/cta.tsx index fa2e68d..3a50492 100644 --- a/components/cta.tsx +++ b/components/cta.tsx @@ -43,7 +43,6 @@ const CTA = ({ title, description, buttonText }: CTAProps) => { ), }); - console.log(values); } return (
diff --git a/components/post-preview.tsx b/components/post-preview.tsx index 20b6bf0..e97782d 100644 --- a/components/post-preview.tsx +++ b/components/post-preview.tsx @@ -14,7 +14,7 @@ const PostPreview = ({ post }: PostPreviewProps) => { return (
{
{post?.tags && (