mirror of
https://github.com/saicaca/fuwari.git
synced 2026-01-11 14:52:52 +01:00
fix: Trim whitespace from category and tag names in URL generation (#437)
Some checks failed
Code quality / quality (push) Failing after 4s
Build and Check / Astro Check for Node.js 22 (push) Failing after 4s
Build and Check / Astro Check for Node.js 23 (push) Failing after 4s
Build and Check / Astro Build for Node.js 22 (push) Failing after 4s
Build and Check / Astro Build for Node.js 23 (push) Failing after 3s
Some checks failed
Code quality / quality (push) Failing after 4s
Build and Check / Astro Check for Node.js 22 (push) Failing after 4s
Build and Check / Astro Check for Node.js 23 (push) Failing after 4s
Build and Check / Astro Build for Node.js 22 (push) Failing after 4s
Build and Check / Astro Build for Node.js 23 (push) Failing after 3s
This commit is contained in:
committed by
GitHub
parent
7f0c109b17
commit
2b3d7cf304
@@ -3,7 +3,7 @@ import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { formatDateToYYYYMMDD } from "../utils/date-utils";
|
||||
import { url } from "../utils/url-utils";
|
||||
import { url, getTagUrl } from "../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
@@ -70,10 +70,10 @@ const className = Astro.props.class;
|
||||
<div class="flex flex-row flex-nowrap items-center">
|
||||
{(tags && tags.length > 0) && tags.map((tag, i) => (
|
||||
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
|
||||
<a href={url(`/archive/tag/${encodeURIComponent(tag)}/`)} aria-label={`View all posts with the ${tag} tag`}
|
||||
<a href={getTagUrl(tag.trim())} aria-label={`View all posts with the ${tag.trim()} tag`}
|
||||
class="link-lg transition text-50 text-sm font-medium
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
|
||||
{tag}
|
||||
{tag.trim()}
|
||||
</a>
|
||||
))}
|
||||
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
|
||||
|
||||
@@ -27,11 +27,11 @@ const style = Astro.props.style;
|
||||
>
|
||||
{categories.map((c) =>
|
||||
<ButtonLink
|
||||
url={getCategoryUrl(c.name)}
|
||||
url={getCategoryUrl(c.name.trim())}
|
||||
badge={String(c.count)}
|
||||
label={`View all posts in the ${c.name} category`}
|
||||
label={`View all posts in the ${c.name.trim()} category`}
|
||||
>
|
||||
{c.name}
|
||||
{c.name.trim()}
|
||||
</ButtonLink>
|
||||
)}
|
||||
</WidgetLayout>
|
||||
@@ -3,7 +3,7 @@
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { getTagList } from "../../utils/content-utils";
|
||||
import { url } from "../../utils/url-utils";
|
||||
import { getTagUrl } from "../../utils/url-utils";
|
||||
import ButtonTag from "../control/ButtonTag.astro";
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
|
||||
@@ -23,8 +23,8 @@ const style = Astro.props.style;
|
||||
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{tags.map(t => (
|
||||
<ButtonTag href={url(`/archive/tag/${encodeURIComponent(t.name)}/`)} label={`View all posts with the ${t.name} tag`}>
|
||||
{t.name}
|
||||
<ButtonTag href={getTagUrl(t.name.trim())} label={`View all posts with the ${t.name.trim()} tag`}>
|
||||
{t.name.trim()}
|
||||
</ButtonTag>
|
||||
))}
|
||||
</div>
|
||||
|
||||
12
src/content/posts/cjk-test.md
Normal file
12
src/content/posts/cjk-test.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: CJK edge case for test
|
||||
published: 2025-05-04
|
||||
updated: 2025-05-04
|
||||
description: 'CJK Test'
|
||||
image: ''
|
||||
tags: [C#, テスト, 技术, Fuwari]
|
||||
category: '技术'
|
||||
draft: true
|
||||
---
|
||||
|
||||
CJK Test
|
||||
@@ -7,16 +7,37 @@ import { getCategoryList } from "@utils/content-utils";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = await getCategoryList();
|
||||
return categories.map((category) => {
|
||||
|
||||
const standardPaths = categories.map((category) => {
|
||||
return {
|
||||
params: {
|
||||
category: encodeURIComponent(category.name),
|
||||
category: encodeURIComponent(category.name.trim()),
|
||||
},
|
||||
props: {
|
||||
decodedCategory: category.name.trim(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const nonEncodedCJKPaths = categories
|
||||
.filter((category) =>
|
||||
/[\u3000-\u9fff\uac00-\ud7af\u4e00-\u9faf]/.test(category.name),
|
||||
)
|
||||
.map((category) => ({
|
||||
params: {
|
||||
category: category.name.trim(), // Do not encode CJK characters
|
||||
},
|
||||
props: {
|
||||
decodedCategory: category.name.trim(),
|
||||
},
|
||||
}));
|
||||
|
||||
return [...standardPaths, ...nonEncodedCJKPaths];
|
||||
}
|
||||
|
||||
const category = decodeURIComponent(Astro.params.category as string);
|
||||
const { decodedCategory } = Astro.props;
|
||||
const category =
|
||||
decodedCategory || decodeURIComponent(Astro.params.category as string);
|
||||
---
|
||||
|
||||
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>
|
||||
|
||||
@@ -4,29 +4,61 @@ import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import { getSortedPosts } from "@utils/content-utils";
|
||||
import { decodePathSegment, encodePathSegment } from "@utils/encoding-utils";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getSortedPosts();
|
||||
|
||||
// タグを集めるための Set の型を指定
|
||||
const allTags = posts.reduce<Set<string>>((acc, post) => {
|
||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||
post.data.tags.forEach((tag) => acc.add(tag));
|
||||
if (Array.isArray(post.data.tags)) {
|
||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||
post.data.tags.forEach((tag) => {
|
||||
if (typeof tag === "string") {
|
||||
acc.add(tag.trim());
|
||||
} else {
|
||||
acc.add(String(tag).trim());
|
||||
}
|
||||
});
|
||||
} else if (post.data.tags && typeof post.data.tags === "string") {
|
||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||
(post.data.tags as string)
|
||||
.split(",")
|
||||
.forEach((tag) => acc.add(tag.trim()));
|
||||
}
|
||||
return acc;
|
||||
}, new Set());
|
||||
|
||||
const allTagsArray = Array.from(allTags);
|
||||
|
||||
return allTagsArray.map((tag) => ({
|
||||
// judge if the string is CJK
|
||||
const isCJK = (str: string) =>
|
||||
/[\u3000-\u9fff\uac00-\ud7af\u4e00-\u9faf]/.test(str);
|
||||
|
||||
const standardPaths = allTagsArray.map((tag) => ({
|
||||
params: {
|
||||
tag: encodeURIComponent(tag),
|
||||
tag: encodePathSegment(tag),
|
||||
},
|
||||
props: {
|
||||
decodedTag: tag,
|
||||
},
|
||||
}));
|
||||
|
||||
const nonEncodedCJKPaths = allTagsArray.filter(isCJK).map((tag) => ({
|
||||
params: {
|
||||
tag: tag, // keep CJK characters unencoded
|
||||
},
|
||||
props: {
|
||||
decodedTag: tag,
|
||||
},
|
||||
}));
|
||||
|
||||
return [...standardPaths, ...nonEncodedCJKPaths];
|
||||
}
|
||||
|
||||
const tag = decodeURIComponent(Astro.params.tag as string);
|
||||
const { decodedTag } = Astro.props;
|
||||
const tag = decodedTag || decodePathSegment(Astro.params.tag as string);
|
||||
---
|
||||
|
||||
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>
|
||||
<ArchivePanel tags={[tag]}></ArchivePanel>
|
||||
</MainGridLayout>
|
||||
</MainGridLayout>
|
||||
|
||||
@@ -67,9 +67,13 @@ export async function getCategoryList(): Promise<Category[]> {
|
||||
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
|
||||
return;
|
||||
}
|
||||
count[post.data.category] = count[post.data.category]
|
||||
? count[post.data.category] + 1
|
||||
: 1;
|
||||
|
||||
const categoryName =
|
||||
typeof post.data.category === "string"
|
||||
? post.data.category.trim()
|
||||
: String(post.data.category).trim();
|
||||
|
||||
count[categoryName] = count[categoryName] ? count[categoryName] + 1 : 1;
|
||||
});
|
||||
|
||||
const lst = Object.keys(count).sort((a, b) => {
|
||||
|
||||
32
src/utils/encoding-utils.ts
Normal file
32
src/utils/encoding-utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Utility functions for ensuring consistent URL encoding
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ensure consistent URL encoding across all tags and categories
|
||||
*
|
||||
* @param value The string to encode
|
||||
* @returns The encoded string
|
||||
*/
|
||||
export function encodePathSegment(value: string): string {
|
||||
if (!value) return "";
|
||||
|
||||
return encodeURIComponent(value.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode from the URL path
|
||||
*
|
||||
* @param value String to decode
|
||||
* @returns Decoded string
|
||||
*/
|
||||
export function decodePathSegment(value: string): string {
|
||||
if (!value) return "";
|
||||
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (e) {
|
||||
console.error(`Failed to decode path segment: ${value}`, e);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import i18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import { encodePathSegment } from "./encoding-utils";
|
||||
|
||||
export function pathsEqual(path1: string, path2: string) {
|
||||
const normalizedPath1 = path1.replace(/^\/|\/$/g, "").toLowerCase();
|
||||
@@ -16,10 +17,27 @@ export function getPostUrlBySlug(slug: string): string {
|
||||
return url(`/posts/${slug}/`);
|
||||
}
|
||||
|
||||
export function getTagUrl(tag: string): string {
|
||||
if (!tag) return url("/archive/tag/");
|
||||
|
||||
// use common encoding function
|
||||
const encodedTag = encodePathSegment(tag);
|
||||
const tagUrl = `/archive/tag/${encodedTag}/`;
|
||||
console.log(`Generating URL for tag "${tag.trim()}" => "${tagUrl}"`);
|
||||
return url(tagUrl);
|
||||
}
|
||||
|
||||
export function getCategoryUrl(category: string): string {
|
||||
if (category === i18n(i18nKey.uncategorized))
|
||||
console.log(`category: ${category}`);
|
||||
if (!category) return url("/archive/category/");
|
||||
|
||||
const trimmedCategory = category.trim();
|
||||
if (trimmedCategory === i18n(i18nKey.uncategorized))
|
||||
return url("/archive/category/uncategorized/");
|
||||
return url(`/archive/category/${encodeURIComponent(category)}/`);
|
||||
|
||||
return url(
|
||||
`/archive/category/${encodeURIComponent(trimmedCategory).replace(/%20/g, "+")}/`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDir(path: string): string {
|
||||
|
||||
Reference in New Issue
Block a user