fix: unable to list uncategorized posts on the archive page (fix #471)

This commit is contained in:
saicaca
2025-05-30 19:42:45 +08:00
parent 4b30ff2b7b
commit dde7835dd7
7 changed files with 17 additions and 130 deletions

View File

@@ -1,118 +0,0 @@
---
import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getSortedPosts } from "../utils/content-utils";
import { getPostUrlBySlug } from "../utils/url-utils";
interface Props {
keyword?: string;
tags?: string[];
categories?: string[];
}
const { tags, categories } = Astro.props;
let posts = await getSortedPosts();
if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (Array.isArray(categories) && categories.length > 0) {
posts = posts.filter(
(post) =>
(post.data.category && categories.includes(post.data.category)) ||
(!post.data.category && categories.includes(UNCATEGORIZED)),
);
}
const groups: { year: number; posts: typeof posts }[] = (() => {
const groupedPosts = posts.reduce(
(grouped: { [year: number]: typeof posts }, post) => {
const year = post.data.published.getFullYear();
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(post);
return grouped;
},
{},
);
// convert the object to an array
const groupedPostsArray = Object.keys(groupedPosts).map((key) => ({
year: Number.parseInt(key),
posts: groupedPosts[Number.parseInt(key)],
}));
// sort years by latest first
groupedPostsArray.sort((a, b) => b.year - a.year);
return groupedPostsArray;
})();
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}`;
}
function formatTag(tag: string[]) {
return tag.map((t) => `#${t}`).join(" ");
}
---
<div class="card-base px-8 py-6">
{
groups.map(group => (
<div>
<div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">{group.year}</div>
<div class="w-[15%] md:w-[10%]">
<div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div>
</div>
<div class="w-[70%] md:w-[80%] transition text-left text-50">{group.posts.length} {i18n(I18nKey.postsCount)}</div>
</div>
{group.posts.map(post => (
<a href={getPostUrlBySlug(post.slug)}
aria-label={post.data.title}
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]
"
></div>
</div>
<!-- post title -->
<div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
>
{post.data.title}
</div>
<!-- tag list -->
<div class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden
text-30"
>{formatTag(post.data.tags)}</div>
</div>
</a>
))}
</div>
))
}
</div>

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from "../i18n/i18nKey"; import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation"; import { i18n } from "../i18n/translation";
import { getPostUrlBySlug } from "../utils/url-utils"; import { getPostUrlBySlug } from "../utils/url-utils";
@@ -13,6 +12,7 @@ export let sortedPosts: Post[] = [];
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
tags = params.has("tag") ? params.getAll("tag") : []; tags = params.has("tag") ? params.getAll("tag") : [];
categories = params.has("category") ? params.getAll("category") : []; categories = params.has("category") ? params.getAll("category") : [];
const uncategorized = params.get("uncategorized");
interface Post { interface Post {
slug: string; slug: string;
@@ -54,12 +54,14 @@ onMount(async () => {
if (categories.length > 0) { if (categories.length > 0) {
filteredPosts = filteredPosts.filter( filteredPosts = filteredPosts.filter(
(post) => (post) => post.data.category && categories.includes(post.data.category),
(post.data.category && categories.includes(post.data.category)) ||
(!post.data.category && categories.includes(UNCATEGORIZED)),
); );
} }
if (uncategorized) {
filteredPosts = filteredPosts.filter((post) => !post.data.category);
}
const grouped = filteredPosts.reduce( const grouped = filteredPosts.reduce(
(acc, post) => { (acc, post) => {
const year = post.data.published.getFullYear(); const year = post.data.published.getFullYear();

View File

@@ -4,7 +4,6 @@ import WidgetLayout from "./WidgetLayout.astro";
import I18nKey from "../../i18n/i18nKey"; import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation"; import { i18n } from "../../i18n/translation";
import { getCategoryList } from "../../utils/content-utils"; import { getCategoryList } from "../../utils/content-utils";
import { getCategoryUrl } from "../../utils/url-utils";
import ButtonLink from "../control/ButtonLink.astro"; import ButtonLink from "../control/ButtonLink.astro";
const categories = await getCategoryList(); const categories = await getCategoryList();
@@ -27,7 +26,7 @@ const style = Astro.props.style;
> >
{categories.map((c) => {categories.map((c) =>
<ButtonLink <ButtonLink
url={getCategoryUrl(c.name)} url={c.url}
badge={String(c.count)} badge={String(c.count)}
label={`View all posts in the ${c.name.trim()} category`} label={`View all posts in the ${c.name.trim()} category`}
> >

View File

@@ -1,5 +1,3 @@
export const UNCATEGORIZED = "__uncategorized__";
export const PAGE_SIZE = 8; export const PAGE_SIZE = 8;
export const LIGHT_MODE = "light", export const LIGHT_MODE = "light",

View File

@@ -9,7 +9,7 @@ const postsCollection = defineCollection({
description: z.string().optional().default(""), description: z.string().optional().default(""),
image: z.string().optional().default(""), image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]), tags: z.array(z.string()).optional().default([]),
category: z.string().optional().default(""), category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""), lang: z.string().optional().default(""),
/* For internal use */ /* For internal use */

View File

@@ -1,6 +1,7 @@
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import I18nKey from "@i18n/i18nKey"; import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation"; import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
export async function getSortedPosts() { export async function getSortedPosts() {
const allBlogPosts = await getCollection("posts", ({ data }) => { const allBlogPosts = await getCollection("posts", ({ data }) => {
@@ -54,6 +55,7 @@ export async function getTagList(): Promise<Tag[]> {
export type Category = { export type Category = {
name: string; name: string;
count: number; count: number;
url: string;
}; };
export async function getCategoryList(): Promise<Category[]> { export async function getCategoryList(): Promise<Category[]> {
@@ -61,7 +63,7 @@ export async function getCategoryList(): Promise<Category[]> {
return import.meta.env.PROD ? data.draft !== true : true; return import.meta.env.PROD ? data.draft !== true : true;
}); });
const count: { [key: string]: number } = {}; const count: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { category: string | number } }) => { allBlogPosts.map((post: { data: { category: string | null } }) => {
if (!post.data.category) { if (!post.data.category) {
const ucKey = i18n(I18nKey.uncategorized); const ucKey = i18n(I18nKey.uncategorized);
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1; count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
@@ -82,7 +84,11 @@ export async function getCategoryList(): Promise<Category[]> {
const ret: Category[] = []; const ret: Category[] = [];
for (const c of lst) { for (const c of lst) {
ret.push({ name: c, count: count[c] }); ret.push({
name: c,
count: count[c],
url: getCategoryUrl(c),
});
} }
return ret; return ret;
} }

View File

@@ -27,7 +27,7 @@ export function getCategoryUrl(category: string): string {
category.trim() === "" || category.trim() === "" ||
category.trim().toLowerCase() === i18n(I18nKey.uncategorized).toLowerCase() category.trim().toLowerCase() === i18n(I18nKey.uncategorized).toLowerCase()
) )
return url("/archive/"); return url("/archive/?uncategorized=true");
return url(`/archive/?category=${encodeURIComponent(category.trim())}`); return url(`/archive/?category=${encodeURIComponent(category.trim())}`);
} }