diff --git a/frontend/src/lib/help.test.ts b/frontend/src/lib/help.test.ts
index a4b246b..79ee9b0 100644
--- a/frontend/src/lib/help.test.ts
+++ b/frontend/src/lib/help.test.ts
@@ -4,6 +4,18 @@ import { createHelpFixture } from '@/test/helpFixtures'
describe ('Help file catalogue', () => {
+ it.each ([['Éclair', 'eclair'], ['home', 'HOME'], ['e\u0301clair', 'Éclair']]) (
+ 'compares title %s and %s equally at primary strength', (a, b) => {
+ const { api } = createHelpFixture ()
+ expect (api.compareHelpTitles (a, b)).toBe (0)
+ })
+
+ it ('orders distinct Latin titles in both directions', () => {
+ const { api } = createHelpFixture ()
+ expect (api.compareHelpTitles ('Apple', 'Zebra')).toBeLessThan (0)
+ expect (api.compareHelpTitles ('Zebra', 'Apple')).toBeGreaterThan (0)
+ })
+
it.each ([['ほーむ', 'ホーム'], ['ホーム', 'ほーむ'], ['ホーム', 'ホーム'],
['ABC', 'abc'], ['ガイド', 'がいど']]) (
'normalises search text %s and %s identically', (query, text) => {
diff --git a/frontend/src/lib/help.ts b/frontend/src/lib/help.ts
index de61501..36b6207 100644
--- a/frontend/src/lib/help.ts
+++ b/frontend/src/lib/help.ts
@@ -33,6 +33,14 @@ export type HelpDiff = {
export type HelpEditionIdentifier = { date: string; revision: number | null }
+// Fix the locale and primary strength; ICU versions still differ from MySQL UCA 9.0.
+const helpTitleCollator = new Intl.Collator ('en', {
+ usage: 'sort', sensitivity: 'base', ignorePunctuation: false, numeric: false })
+
+
+export const compareHelpTitles = (a: string, b: string): number =>
+ helpTitleCollator.compare (a, b)
+
export const normaliseHelpSearchText = (value: string): string =>
value.normalize ('NFKC').toLowerCase ().replace (/[ァ-ヶヽヾ]/g,
diff --git a/frontend/src/pages/help/HelpListPage.test.tsx b/frontend/src/pages/help/HelpListPage.test.tsx
index 385ad79..218ccd1 100644
--- a/frontend/src/pages/help/HelpListPage.test.tsx
+++ b/frontend/src/pages/help/HelpListPage.test.tsx
@@ -90,6 +90,20 @@ describe ('Help search', () => {
})
// ai_ci equality is documented by MySQL; these are not captured DB sort results.
+ it.each (['asc', 'desc']) ('orders different titles before identifiers in order %s',
+ async direction => {
+ helpFixture.assets.search.default.ja = [
+ { identifier: 'alpha', edition: '2026-10-02', title: 'Zebra', body: '' },
+ { identifier: 'beta', edition: '2026-10-02', title: 'Apple', body: '' }]
+ const { container } = renderWithProviders (, {
+ route: `/help?order=title:${ direction }` })
+ await screen.findByText ('2 件')
+ const expected = direction === 'asc'
+ ? ['/help/beta', '/help/alpha'] : ['/help/alpha', '/help/beta']
+ expect (Array.from (container.querySelectorAll ('tbody a'), link =>
+ link.getAttribute ('href'))).toEqual (expected)
+ })
+
it.each (['asc', 'desc']) ('uses identifier ties for MySQL ai_ci title order %s',
async direction => {
helpFixture.assets.search.default.ja = [
diff --git a/frontend/src/pages/help/HelpListPage.tsx b/frontend/src/pages/help/HelpListPage.tsx
index 0b4976d..bf0341c 100644
--- a/frontend/src/pages/help/HelpListPage.tsx
+++ b/frontend/src/pages/help/HelpListPage.tsx
@@ -11,6 +11,7 @@ import Pagination from '@/components/common/Pagination'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { compareHelpEditions,
+ compareHelpTitles,
getHelpSearchEntries,
helpEditionDate,
normaliseHelpSearchText } from '@/lib/help'
@@ -70,7 +71,7 @@ const HelpSearchPage: FC = () => {
|| normaliseHelpSearchText (entry.body).includes (textQuery))))
.sort ((a, b) => {
const comparison = field === 'title'
- ? a.title.localeCompare (b.title, 'ja')
+ ? compareHelpTitles (a.title, b.title)
: compareHelpEditions (a.edition, b.edition)
return (direction === 'asc' ? comparison : -comparison)
diff --git a/frontend/src/pages/wiki/WikiDetailPage.test.tsx b/frontend/src/pages/wiki/WikiDetailPage.test.tsx
index fac93da..84268c9 100644
--- a/frontend/src/pages/wiki/WikiDetailPage.test.tsx
+++ b/frontend/src/pages/wiki/WikiDetailPage.test.tsx
@@ -1,9 +1,10 @@
-import { act, fireEvent, screen, waitFor } from '@testing-library/react'
-import { Route, Routes } from 'react-router-dom'
+import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
+import { Route, Routes, useLocation } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { apiPost } from '@/lib/api'
import { getHelpTitleIndex } from '@/lib/help'
+import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
import WikiNewPage from '@/pages/wiki/WikiNewPage'
import { buildTag, buildUser, buildWikiPage } from '@/test/factories'
@@ -27,7 +28,8 @@ vi.mock ('@/lib/wiki', () => wikiApi)
vi.mock ('@/lib/tags', () => tagsApi)
vi.mock ('@/lib/posts', () => postsApi)
vi.mock ('@/lib/help', () => ({ getHelpTitleIndex: vi.fn () }))
-vi.mock ('@/lib/api', () => ({ apiPost: vi.fn () }))
+vi.mock ('@/lib/api', async importOriginal => ({
+ ...await importOriginal (), apiPost: vi.fn () }))
vi.mock ('@/components/ui/use-toast', () => ({ toast: vi.fn () }))
vi.mock ('react-markdown-editor-lite', () => ({
default: ({ value, onChange }: {
@@ -36,6 +38,13 @@ vi.mock ('react-markdown-editor-lite', () => ({