mirror of
https://github.com/KazooTTT/kazoottt-blog.git
synced 2025-06-22 10:11:30 +08:00
feat: add category page
This commit is contained in:
@ -7,18 +7,42 @@
|
||||
class='relative mx-auto flex w-full items-center justify-between sm:flex sm:items-center'
|
||||
aria-label='global'
|
||||
>
|
||||
<a class='flex-none text-xl font-semibold' href='/' aria-label='Brand'>resume</a>
|
||||
<a class='flex-none text-xl font-semibold' href='/' aria-label='Brand'>KazooTTT Blog</a>
|
||||
|
||||
<div class='flex flex-row items-center justify-center gap-x-5 sm:gap-x-7'>
|
||||
<a
|
||||
href='/blog'
|
||||
class='flex-none text-[1.05rem] font-medium hover:text-foreground/75'
|
||||
class={`
|
||||
flex-none text-[1.05rem] font-medium hover:text-foreground/75
|
||||
${Astro.url.pathname.startsWith('/blog') ? 'text-green-400' : ''}
|
||||
`}
|
||||
aria-label='Nav Menu Item'
|
||||
>Blog
|
||||
</a>
|
||||
<a
|
||||
href='/categories'
|
||||
class={`
|
||||
flex-none text-[1.05rem] font-medium hover:text-foreground/75
|
||||
${Astro.url.pathname.startsWith('/categories') ? 'text-green-400' : ''}
|
||||
`}
|
||||
aria-label='Nav Menu Item'
|
||||
>Categories
|
||||
</a>
|
||||
<a
|
||||
href='/tags'
|
||||
class={`
|
||||
flex-none text-[1.05rem] font-medium hover:text-foreground/75
|
||||
${Astro.url.pathname.startsWith('/tags') ? 'text-green-400' : ''}
|
||||
`}
|
||||
aria-label='Nav Menu Item'
|
||||
>Tags
|
||||
</a>
|
||||
<a
|
||||
href='/tools'
|
||||
class='flex-none text-[1.05rem] font-medium hover:text-foreground/75'
|
||||
class={`
|
||||
flex-none text-[1.05rem] font-medium hover:text-foreground/75
|
||||
${Astro.url.pathname.startsWith('/tools') ? 'text-green-400' : ''}
|
||||
`}
|
||||
aria-label='Nav Menu Item'
|
||||
>Tools
|
||||
</a>
|
||||
|
@ -12,7 +12,7 @@ const post = defineCollection({
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z.string().max(60),
|
||||
description: z.string().min(50).max(160),
|
||||
description: z.string(),
|
||||
publishDate: z
|
||||
.string()
|
||||
.or(z.date())
|
||||
@ -29,7 +29,8 @@ const post = defineCollection({
|
||||
.optional(),
|
||||
draft: z.boolean().default(false),
|
||||
tags: z.array(z.string()).default([]).transform(removeDupsAndLowerCase),
|
||||
ogImage: z.string().optional()
|
||||
ogImage: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -7,4 +7,5 @@ coverImage:
|
||||
src: "./cover.png"
|
||||
alt: "Astro build wallpaper"
|
||||
tags: ["test", "image"]
|
||||
category: c1
|
||||
---
|
||||
|
@ -4,6 +4,7 @@ description: "This post is for testing the draft post functionality"
|
||||
publishDate: "10 Sept 2023"
|
||||
tags: ["test"]
|
||||
draft: true
|
||||
category: c2
|
||||
---
|
||||
|
||||
If this is working correctly, this post should only be accessible in a dev environment, as well as any tags that are unique to this post.
|
||||
|
@ -3,6 +3,7 @@ title: "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Id"
|
||||
description: "This post is purely for testing if the css is correct for the title on the page"
|
||||
publishDate: "01 Feb 2023"
|
||||
tags: ["test"]
|
||||
category: c3
|
||||
---
|
||||
|
||||
## Testing the title tag
|
||||
|
@ -8,21 +8,23 @@ import Button from '@/components/Button.astro'
|
||||
import Pagination from '@/components/Paginator.astro'
|
||||
import PostPreview from '@/components/blog/PostPreview.astro'
|
||||
import PageLayout from '@/layouts/BaseLayout.astro'
|
||||
import { getAllPosts, getUniqueTags, sortMDByDate } from '@/utils'
|
||||
import { getAllPosts, getUniqueCategories, getUniqueTags, sortMDByDate } from '@/utils'
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const allPosts = await getAllPosts()
|
||||
const allPostsByDate = sortMDByDate(allPosts)
|
||||
const uniqueTags = getUniqueTags(allPosts)
|
||||
return paginate(allPostsByDate, { pageSize: 10, props: { uniqueTags } })
|
||||
const uniqueCategories = getUniqueCategories(allPosts)
|
||||
return paginate(allPostsByDate, { pageSize: 10, props: { uniqueTags, uniqueCategories } })
|
||||
}) satisfies GetStaticPaths
|
||||
|
||||
interface Props {
|
||||
page: Page<CollectionEntry<'post'>>
|
||||
uniqueTags: string[]
|
||||
uniqueCategories: string[]
|
||||
}
|
||||
|
||||
const { page, uniqueTags } = Astro.props
|
||||
const { page, uniqueTags, uniqueCategories } = Astro.props
|
||||
|
||||
const meta = {
|
||||
description: 'Posts',
|
||||
@ -77,41 +79,89 @@ const paginationProps = {
|
||||
</ul>
|
||||
<Pagination {...paginationProps} />
|
||||
</section>
|
||||
{!!uniqueTags.length && (
|
||||
<aside>
|
||||
<h2 class='mb-4 flex items-center text-lg font-semibold'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
class='h-6 w-6'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
stroke-linecap='round'
|
||||
stroke-linejoin='round'
|
||||
stroke-width='1.5'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path d='M0 0h24v24H0z' fill='none' stroke='none' />
|
||||
<path d='M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z' />
|
||||
<path d='M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116' />
|
||||
<path d='M6 9h-.01' />
|
||||
</svg>
|
||||
Tags
|
||||
</h2>
|
||||
<ul class='text-bgColor flex flex-wrap gap-2'>
|
||||
{uniqueTags.map((tag) => (
|
||||
<li>
|
||||
<Button title={tag} href={`/tags/${tag}/`} style='pill' />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<span class='mt-4 block sm:text-end'>
|
||||
<a aria-label='View all blog categories' class='' href='/tags/' data-astro-prefetch>
|
||||
View all →
|
||||
</a>
|
||||
</span>
|
||||
</aside>
|
||||
)}
|
||||
<div>
|
||||
{!!uniqueCategories.length && (
|
||||
<aside>
|
||||
<h2 class='mb-4 flex items-center text-lg font-semibold'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
class='h-6 w-6'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
stroke-linecap='round'
|
||||
stroke-linejoin='round'
|
||||
stroke-width='1.5'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path d='M0 0h24v24H0z' fill='none' stroke='none' />
|
||||
<path d='M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z' />
|
||||
<path d='M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116' />
|
||||
<path d='M6 9h-.01' />
|
||||
</svg>
|
||||
Categories
|
||||
</h2>
|
||||
<ul class='text-bgColor flex flex-wrap gap-2'>
|
||||
{uniqueCategories.slice(0, 6).map((category) => (
|
||||
<li>
|
||||
<Button title={category} href={`/categories/${category}/`} style='pill' />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<span class='mt-4 block sm:text-end'>
|
||||
<a
|
||||
aria-label='View all blog categories'
|
||||
class=''
|
||||
href='/categories/'
|
||||
data-astro-prefetch
|
||||
>
|
||||
View all →
|
||||
</a>
|
||||
</span>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{!!uniqueTags.length && (
|
||||
<aside>
|
||||
<h2 class='mb-4 flex items-center text-lg font-semibold'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
class='h-6 w-6'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
stroke-linecap='round'
|
||||
stroke-linejoin='round'
|
||||
stroke-width='1.5'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path d='M0 0h24v24H0z' fill='none' stroke='none' />
|
||||
<path d='M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z' />
|
||||
<path d='M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116' />
|
||||
<path d='M6 9h-.01' />
|
||||
</svg>
|
||||
Tags
|
||||
</h2>
|
||||
<ul class='text-bgColor flex flex-wrap gap-2'>
|
||||
{uniqueTags.slice(0, 6).map((tag) => (
|
||||
<li>
|
||||
<Button title={tag} href={`/tags/${tag}/`} style='pill' />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<span class='mt-4 block sm:text-end'>
|
||||
<a
|
||||
aria-label='View all blog categories'
|
||||
class=''
|
||||
href='/tags/'
|
||||
data-astro-prefetch
|
||||
>
|
||||
View all →
|
||||
</a>
|
||||
</span>
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
96
src/pages/categories/[category]/[...page].astro
Normal file
96
src/pages/categories/[category]/[...page].astro
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
export const prerender = true
|
||||
|
||||
import type { GetStaticPaths, Page } from 'astro'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
import Pagination from '@/components/Paginator.astro'
|
||||
import PostPreview from '@/components/blog/PostPreview.astro'
|
||||
import PageLayout from '@/layouts/BaseLayout.astro'
|
||||
import Button from '@/components/Button.astro'
|
||||
import { getAllPosts, getUniqueTags, sortMDByDate } from '@/utils'
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
|
||||
const allPosts = await getAllPosts()
|
||||
const allPostsByDate = sortMDByDate(allPosts)
|
||||
const uniqueTags = getUniqueTags(allPostsByDate)
|
||||
|
||||
return uniqueTags.flatMap((tag) => {
|
||||
const filterPosts = allPostsByDate.filter((post) => post.data.tags.includes(tag))
|
||||
return paginate(filterPosts, {
|
||||
pageSize: 10,
|
||||
params: { tag }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
interface Props {
|
||||
page: Page<CollectionEntry<'post'>>
|
||||
}
|
||||
|
||||
const { page } = Astro.props
|
||||
const { tag } = Astro.params
|
||||
|
||||
const meta = {
|
||||
description: `View all posts with the tag - ${tag}`,
|
||||
title: `Tag: ${tag}`
|
||||
}
|
||||
|
||||
const paginationProps = {
|
||||
...(page.url.prev && {
|
||||
prevUrl: {
|
||||
text: `← Previous Tags`,
|
||||
url: page.url.prev
|
||||
}
|
||||
}),
|
||||
...(page.url.next && {
|
||||
nextUrl: {
|
||||
text: `Next Tags →`,
|
||||
url: page.url.next
|
||||
}
|
||||
})
|
||||
}
|
||||
---
|
||||
|
||||
<PageLayout meta={meta}>
|
||||
<div class='w-full'>
|
||||
<Button title='Back' href='/blog' style='button'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='14'
|
||||
height='14'
|
||||
viewBox='0 0 24 24'
|
||||
slot='icon-before'
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='m6.921 12.5l5.792 5.792L12 19l-7-7l7-7l.713.708L6.921 11.5H19v1z'
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</Button>
|
||||
<h1 class='mb-6 mt-5 flex items-end gap-x-2 text-2xl font-bold'>
|
||||
Tags:
|
||||
<span class='text-xl'>#{tag}</span>
|
||||
</h1>
|
||||
<section aria-label='Blog post list'>
|
||||
<ul class='flex flex-col gap-y-3 text-start'>
|
||||
{page.data.map((p) => <PostPreview as='h2' post={p} withDesc />)}
|
||||
</ul>
|
||||
<Pagination {...paginationProps} />
|
||||
</section>
|
||||
<a
|
||||
href='https://github.com/srleom/astro-theme-resume.git'
|
||||
class='mt-16 inline-flex flex-row items-center gap-x-3 rounded-3xl border border-input px-4 py-2 text-sm shadow-sm transition-all hover:shadow-md'
|
||||
>
|
||||
<span class='relative flex items-center justify-center'>
|
||||
<span
|
||||
class='absolute inline-flex h-2 w-2 animate-ping rounded-full border border-green-400 bg-green-400 opacity-75'
|
||||
></span>
|
||||
<span class='relative inline-flex h-2 w-2 rounded-full bg-green-400'></span>
|
||||
</span>
|
||||
|
||||
<p class='font-medium'>Get free template</p>
|
||||
</a>
|
||||
</div>
|
||||
</PageLayout>
|
72
src/pages/categories/index.astro
Normal file
72
src/pages/categories/index.astro
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
import Button from '@/components/Button.astro'
|
||||
import PageLayout from '@/layouts/BaseLayout.astro'
|
||||
import { getAllPosts, getUniqueCategoriesWithCount } from '@/utils'
|
||||
|
||||
const allPosts = await getAllPosts()
|
||||
const allCategories = getUniqueCategoriesWithCount(allPosts)
|
||||
|
||||
const meta = {
|
||||
description: "A list of all the topics I've written about in my posts",
|
||||
title: 'All Categories'
|
||||
}
|
||||
---
|
||||
|
||||
<PageLayout meta={meta}>
|
||||
<div class='w-full'>
|
||||
<Button title='Back' href='/blog' style='button'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='14'
|
||||
height='14'
|
||||
viewBox='0 0 24 24'
|
||||
slot='icon-before'
|
||||
>
|
||||
<path
|
||||
fill='currentColor'
|
||||
d='m6.921 12.5l5.792 5.792L12 19l-7-7l7-7l.713.708L6.921 11.5H19v1z'
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
<h1 class='mb-6 mt-5 text-2xl font-bold'>Categories</h1>
|
||||
{allCategories.length === 0 && <p>No posts yet.</p>}
|
||||
|
||||
{
|
||||
allCategories.length > 0 && (
|
||||
<ul class='flex flex-col gap-y-3'>
|
||||
{allCategories.map(([tag, val]) => (
|
||||
<li class='flex items-center gap-x-2 '>
|
||||
<a
|
||||
class='inline-block underline underline-offset-4 hover:text-foreground/75'
|
||||
data-astro-prefetch
|
||||
href={`/tags/${tag}/`}
|
||||
title={`View posts with the tag: ${tag}`}
|
||||
>
|
||||
#{tag}
|
||||
</a>
|
||||
<span class='inline-block'>
|
||||
- {val} post{val > 1 && 's'}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
<a
|
||||
href='https://github.com/srleom/astro-theme-resume.git'
|
||||
class='mt-16 inline-flex flex-row items-center gap-x-3 rounded-3xl border border-input px-4 py-2 text-sm shadow-sm transition-all hover:shadow-md'
|
||||
>
|
||||
<span class='relative flex items-center justify-center'>
|
||||
<span
|
||||
class='absolute inline-flex h-2 w-2 animate-ping rounded-full border border-green-400 bg-green-400 opacity-75'
|
||||
></span>
|
||||
<span class='relative inline-flex h-2 w-2 rounded-full bg-green-400'></span>
|
||||
</span>
|
||||
|
||||
<p class='font-medium'>Get free template</p>
|
||||
</a>
|
||||
</div>
|
||||
</PageLayout>
|
@ -3,18 +3,18 @@ import type { AstroExpressiveCodeOptions } from 'astro-expressive-code'
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
// Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts)
|
||||
author: 'SRLEOM',
|
||||
author: 'KazooTTT',
|
||||
// Meta property used to construct the meta title property, found in src/components/BaseHead.astro L:11
|
||||
title: 'astro-theme-resume',
|
||||
title: 'KazooTTT Blog',
|
||||
// Meta property used as the default description meta property
|
||||
description: 'The official Astro Resume Theme',
|
||||
description: '声控烤箱 | 我喜欢的烤箱是声控的 | 前端小透明',
|
||||
// HTML lang property, found in src/layouts/Base.astro L:18
|
||||
lang: 'en-GB',
|
||||
lang: 'zh-CN',
|
||||
// Meta property, found in src/components/BaseHead.astro L:42
|
||||
ogLocale: 'en_GB',
|
||||
ogLocale: 'zh-CN',
|
||||
// Date.prototype.toLocaleDateString() parameters, found in src/utils/date.ts.
|
||||
date: {
|
||||
locale: 'en-GB',
|
||||
locale: 'zh-CN',
|
||||
options: {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
@ -31,7 +31,7 @@ export const menuLinks: Array<{ title: string; path: string }> = [
|
||||
{
|
||||
title: 'Blog',
|
||||
path: '/blog/'
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// https://expressive-code.com/reference/configuration/
|
||||
|
@ -1,5 +1,5 @@
|
||||
export { cn } from './tailwind'
|
||||
export { getAllPosts, sortMDByDate, getUniqueTags, getUniqueTagsWithCount } from './post'
|
||||
export { getAllPosts, sortMDByDate, getUniqueTags, getUniqueTagsWithCount, getAllCategories, getUniqueCategories,getUniqueCategoriesWithCount } from './post'
|
||||
export { getFormattedDate } from './date'
|
||||
export { generateToc } from './generateToc'
|
||||
export type { TocItem } from './generateToc'
|
||||
|
@ -37,3 +37,26 @@ export function getUniqueTagsWithCount(
|
||||
)
|
||||
].sort((a, b) => b[1] - a[1])
|
||||
}
|
||||
|
||||
/** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */
|
||||
export function getAllCategories(posts: Array<CollectionEntry<'post'>>): string[] {
|
||||
return posts.map(post => post.data.category ?? "未分类")
|
||||
}
|
||||
|
||||
|
||||
/** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */
|
||||
export function getUniqueCategories(posts: Array<CollectionEntry<'post'>>) {
|
||||
return [...new Set(getAllCategories(posts))]
|
||||
}
|
||||
|
||||
/** Note: This function doesn't filter draft posts, pass it the result of getAllPosts above to do so. */
|
||||
export function getUniqueCategoriesWithCount(
|
||||
posts: Array<CollectionEntry<'post'>>
|
||||
): Array<[string, number]> {
|
||||
return [
|
||||
...getAllCategories(posts).reduce(
|
||||
(acc, t) => acc.set(t, (acc.get(t) || 0) + 1),
|
||||
new Map<string, number>()
|
||||
)
|
||||
].sort((a, b) => b[1] - a[1])
|
||||
}
|
Reference in New Issue
Block a user