Files
blog-nextjs/lib/posts.ts
gbanyan 5d3d754252 Fix tag URL encoding for non-ASCII characters
Updated getTagSlug() to properly encode tags with spaces and non-ASCII characters (like Chinese). The function now:
- Normalizes multiple spaces/dashes to single dashes
- Properly encodes non-ASCII characters using encodeURIComponent
- Prevents issues with URL encoding on Vercel deployment

This fixes tags like "Medicine - 醫學" being displayed as "medicine---%E9%86%AB%E5%AD%B8" by generating clean URLs like "medicine-%E9%86%AB%E5%AD%B8".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 23:10:34 +08:00

112 lines
3.2 KiB
TypeScript

import { allPosts, allPages, Post, Page } from 'contentlayer2/generated';
export function getAllPostsSorted(): Post[] {
return [...allPosts].sort((a, b) => {
const aDate = a.published_at ? new Date(a.published_at).getTime() : 0;
const bDate = b.published_at ? new Date(b.published_at).getTime() : 0;
return bDate - aDate;
});
}
export function getPostBySlug(slug: string): Post | undefined {
return allPosts.find(
(post) =>
post.flattenedPath === slug ||
post.slug === slug ||
post._raw.flattenedPath === slug
);
}
export function getPageBySlug(slug: string): Page | undefined {
return allPages.find(
(page) =>
page.flattenedPath === slug ||
page.slug === slug ||
page._raw.flattenedPath === slug
);
}
export function getTagSlug(tag: string): string {
// Normalize spaces and convert to lowercase first
// Replace multiple spaces/dashes with single dash
const normalized = tag
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
// Encode URI components to handle non-ASCII characters properly
return encodeURIComponent(normalized);
}
export function getAllTagsWithCount(): { tag: string; slug: string; count: number }[] {
const map = new Map<string, number>();
for (const post of allPosts) {
if (!post.tags) continue;
for (const tag of post.tags) {
map.set(tag, (map.get(tag) ?? 0) + 1);
}
}
return Array.from(map.entries())
.map(([tag, count]) => ({ tag, slug: getTagSlug(tag), count }))
.sort((a, b) => {
if (b.count === a.count) return a.tag.localeCompare(b.tag);
return b.count - a.count;
});
}
export function getRelatedPosts(target: Post, limit = 3): Post[] {
const targetTags = new Set(target.tags?.map((tag) => tag.toLowerCase()) ?? []);
const candidates = getAllPostsSorted().filter((post) => post._id !== target._id);
if (candidates.length === 0) return [];
const scored = candidates
.map((post) => {
const sharedTags = (post.tags ?? []).reduce((acc, tag) => {
return acc + (targetTags.has(tag.toLowerCase()) ? 1 : 0);
}, 0);
return { post, score: sharedTags };
})
.filter((entry) => entry.score > 0)
.sort((a, b) => {
if (b.score === a.score) {
const aDate = a.post.published_at
? new Date(a.post.published_at).getTime()
: 0;
const bDate = b.post.published_at
? new Date(b.post.published_at).getTime()
: 0;
return bDate - aDate;
}
return b.score - a.score;
})
.slice(0, limit)
.map((entry) => entry.post);
if (scored.length >= limit) {
return scored;
}
const fallback = candidates.filter(
(post) => !scored.some((existing) => existing._id === post._id)
);
return [...scored, ...fallback.slice(0, limit - scored.length)].slice(0, limit);
}
export function getPostNeighbors(target: Post): {
newer?: Post;
older?: Post;
} {
const sorted = getAllPostsSorted();
const index = sorted.findIndex((post) => post._id === target._id);
if (index === -1) return {};
return {
newer: index > 0 ? sorted[index - 1] : undefined,
older: index < sorted.length - 1 ? sorted[index + 1] : undefined
};
}