feat: implement tags system and optimize blog structure

This commit is contained in:
Guoqi Sun 2024-12-12 01:13:28 +08:00
parent dcf1f74a27
commit b2f3ec355a
22 changed files with 151 additions and 71 deletions

View file

@ -28,6 +28,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"@types/dom-view-transitions": "^1.0.5",
"@types/node": "^22.10.2",
"@types/sanitize-html": "^2.13.0",
"@typescript-eslint/parser": "^8.18.0",

29
pnpm-lock.yaml generated
View file

@ -54,6 +54,9 @@ importers:
'@tailwindcss/typography':
specifier: ^0.5.15
version: 0.5.15(tailwindcss@3.4.16)
'@types/dom-view-transitions':
specifier: ^1.0.5
version: 1.0.5
'@types/node':
specifier: ^22.10.2
version: 22.10.2
@ -630,67 +633,79 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@ -817,46 +832,55 @@ packages:
resolution: {integrity: sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.27.4':
resolution: {integrity: sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.27.4':
resolution: {integrity: sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.27.4':
resolution: {integrity: sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.27.4':
resolution: {integrity: sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.27.4':
resolution: {integrity: sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.27.4':
resolution: {integrity: sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.27.4':
resolution: {integrity: sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.27.4':
resolution: {integrity: sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.27.4':
resolution: {integrity: sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==}
@ -911,6 +935,9 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/dom-view-transitions@1.0.5':
resolution: {integrity: sha512-N2sILR7fxSMnaFaAPwGj4DtHCXjIyQTHt+xJyf9jATpzUsTkMNM0DWtqZB6W7f501B/Y0tq3uqat/VlbjuTpMA==}
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@ -4279,6 +4306,8 @@ snapshots:
dependencies:
'@types/ms': 0.7.34
'@types/dom-view-transitions@1.0.5': {}
'@types/estree@1.0.6': {}
'@types/hast@3.0.4':

View file

@ -1,21 +1,14 @@
---
import { config } from "~/config"
import { type Post } from "~/types/post"
import { formatDate, getPosts } from "~/utils"
interface PostProps {
slug: string
data: {
title: string
pubDate: Date
}
}
const posts = (await getPosts()).slice(0, config.latestPosts ?? 8)
---
<div class="mb-4 text-xl font-medium md:my-8">近期文章</div>
{
posts.map((post: PostProps) => (
posts.map((post: Post) => (
<a href={`/posts/${post.slug}/`}>
<div class="my-2 flex text-lg">
<p class="flex w-36 flex-shrink-0 truncate text-gray-500 dark:text-gray-400">

View file

@ -2,4 +2,4 @@
import { config } from "~/config"
---
<div class="my-10">{config.intro}</div>
<div class="mb-10">{config.intro}</div>

View file

@ -1,10 +1,10 @@
---
import { config } from "~/config"
const { home, archive, category, tags, custom, about } = config.navigation
const { home, archive, tags, custom, about } = config.navigation
---
<div class="flex gap-4 text-lg">
<div class="mb-8 flex gap-4 text-lg">
{
home && (
<a href="/" class="hover:underline hover:underline-offset-4">
@ -19,13 +19,6 @@ const { home, archive, category, tags, custom, about } = config.navigation
</a>
)
}
{
category && (
<a href="/category" class="hover:underline hover:underline-offset-4">
<p>分类</p>
</a>
)
}
{
tags && (
<a href="/tags" class="hover:underline hover:underline-offset-4">

View file

@ -8,27 +8,6 @@ import { Moon, Sun } from "lucide-react"
</button>
<script>
// check current theme
const theme = (() => {
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme")
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark"
}
return "light"
})()
// set theme
if (theme === "light") {
document.documentElement.classList.remove("dark")
} else {
document.documentElement.classList.add("dark")
}
// save theme
window.localStorage.setItem("theme", theme!)
// update theme
const updateTheme = () => {
const isDark = document.documentElement.classList.contains("dark")

View file

@ -25,8 +25,7 @@ export const config = {
navigation: {
home: true,
archive: true,
category: false,
tags: false,
tags: true,
about: true,
custom: [
{

View file

@ -8,6 +8,7 @@ const posts = defineCollection({
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).optional(),
}),
})

View file

@ -1,6 +1,6 @@
---
title: "我的第一篇博客文章"
pubDate: 2024-07-01
pubDate: 2022-07-01
description: "这是我 Astro 博客的第一篇文章。"
author: "Astro 学习者"
image:

View file

@ -5,7 +5,7 @@ description: "学习了一些 Astro 后,我根本停不下来!"
image:
url: "https://docs.astro.build/assets/arc.webp"
alt: "The Astro logo on a dark background with a purple gradient arc."
pubDate: 2024-07-08
pubDate: 2023-07-08
tags: ["astro", "blogging", "learning in public", "successes"]
---

View file

@ -5,7 +5,7 @@ description: "我遇到了一些问题,但是在社区里面提问真的很有
image:
url: "https://docs.astro.build/assets/rays.webp"
alt: "The Astro logo on a dark background with rainbow rays."
pubDate: 2024-07-15
pubDate: 2021-07-15
tags: ["astro", "learning in public", "setbacks", "community"]
---

View file

@ -8,7 +8,7 @@ const { title, description, pubDate, updatedDate } = Astro.props
---
<HomeLayout>
<article class="prose mt-10 dark:prose-invert">
<article class="prose my-10 dark:prose-invert">
<slot />
</article>
</HomeLayout>

View file

@ -1,5 +1,5 @@
---
layout: ../layouts/post.astro
layout: ../../layouts/post.astro
title: "关于我"
description: "前端开发者 | 摄影爱好者 | 骑行爱好者"
---

View file

@ -1,7 +0,0 @@
---
import HomeLayout from "~/layouts/home.astro"
---
<HomeLayout>
<div>archive</div>
</HomeLayout>

View file

@ -0,0 +1,45 @@
---
import HomeLayout from "~/layouts/home.astro"
import { type Post } from "~/types/post"
import { formatDate, getPosts } from "~/utils"
const posts = await getPosts()
const postsByYear = posts.reduce(
(acc: Record<string, Post[]>, post: Post) => {
const year = new Date(post.data.pubDate).getFullYear().toString()
if (!acc[year]) {
acc[year] = []
}
acc[year].push(post)
return acc
},
{} as Record<string, Post[]>,
)
const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a))
---
<HomeLayout>
<div class="space-y-8">
{
years.map((year) => (
<div class="year-group">
<h2 class="mb-4 text-2xl font-bold">{year}</h2>
<div class="space-y-2">
{postsByYear[year].map((post: Post) => (
<a href={`/posts/${post.slug}/`}>
<div class="my-2 flex text-lg">
<p class="flex w-36 flex-shrink-0 truncate text-gray-500 dark:text-gray-400">
<time>{formatDate(post.data.pubDate)}</time>
</p>
<p class="line-clamp-3">{post.data.title}</p>
</div>
</a>
))}
</div>
</div>
))
}
</div>
</HomeLayout>

View file

@ -1,7 +0,0 @@
---
import HomeLayout from "~/layouts/home.astro"
---
<HomeLayout>
<div>category</div>
</HomeLayout>

View file

@ -1,6 +1,6 @@
---
import { type CollectionEntry, getCollection } from "astro:content"
import Post from "~/layouts/post.astro"
import PostLayout from "~/layouts/post.astro"
export async function getStaticPaths() {
const posts = await getCollection("posts")
@ -15,6 +15,6 @@ const post = Astro.props
const { Content } = await post.render()
---
<Post {...post.data}>
<PostLayout {...post.data}>
<Content />
</Post>
</PostLayout>

View file

@ -1,7 +0,0 @@
---
import HomeLayout from "~/layouts/home.astro"
---
<HomeLayout>
<div>tag</div>
</HomeLayout>

View file

@ -0,0 +1,35 @@
---
import HomeLayout from "~/layouts/home.astro"
import { formatDate, getPosts } from "~/utils"
export async function getStaticPaths() {
const posts = await getPosts()
const uniqueTags = [...new Set(posts.flatMap((post) => post.data.tags || []))]
return uniqueTags.map((tag) => ({
params: { tag },
props: { posts },
}))
}
const { tag } = Astro.params
const { posts } = Astro.props
const filteredPosts = posts.filter((post) => post.data.tags?.includes(tag))
---
<HomeLayout>
<ul>
{
filteredPosts.map((post) => (
<a href={`/posts/${post.slug}/`}>
<div class="my-2 flex text-lg">
<p class="flex w-36 flex-shrink-0 truncate text-gray-500 dark:text-gray-400">
<time>{formatDate(post.data.pubDate)}</time>
</p>
<p class="line-clamp-3">{post.data.title}</p>
</div>
</a>
))
}
</ul>
</HomeLayout>

View file

@ -0,0 +1,19 @@
---
import HomeLayout from "~/layouts/home.astro"
import { getPosts } from "~/utils"
const posts = await getPosts()
const tags = [...new Set(posts.map((post) => post.data.tags).flat())]
---
<HomeLayout>
<div>
{
tags.map((tag) => (
<span class="mx-4">
<a href={`/tags/${tag}`}>{tag}</a>
</span>
))
}
</div>
</HomeLayout>

7
src/types/post.ts Normal file
View file

@ -0,0 +1,7 @@
export interface Post {
slug: string
data: {
title: string
pubDate: Date
}
}