fix(reader): drop "Chapter N:" prefix from chapter title display (0.51.2)

The chapter list on the manga detail page, the reader's chapter-select
dropdown, the continuous-mode chapter bar, the browser tab title, and
the profile upload-history entries all prepended "Chapter {number}:"
in front of the crawled site title. Source titles already include
"Ch.N" themselves and the manga page renders chapters inside an <ol>,
so the prefix duplicated information the user could already see.

A small chapterLabel(c) helper in $lib/api/chapters returns the site
title as-is, falling back to "Chapter {number}" only when the
crawler captured an empty title (link/option stays non-empty). The
five render sites now call it. The previous-/next-chapter nav
buttons still read "Previous chapter (Ch. N)" / "Next chapter (Ch. N)"
since those are wayfinding labels, not title display.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-03 07:22:17 +02:00
parent e93eec89e5
commit b812c6d16c
9 changed files with 36 additions and 13 deletions

View File

@@ -120,7 +120,7 @@ test('manga overview shows title, cover, and a chapter list', async ({ page }) =
await expect(page.getByTestId('manga-title')).toHaveText('Berserk');
await expect(page.getByTestId('manga-author')).toContainText('Kentaro Miura');
await expect(page.getByTestId('manga-cover')).toBeVisible();
await expect(page.getByTestId('chapter-list')).toContainText('Chapter 1');
await expect(page.getByTestId('chapter-list')).toContainText('The Brand');
await expect(page.getByTestId('bookmark-signin')).toBeVisible();
});

View File

@@ -1,6 +1,6 @@
{
"name": "mangalord-frontend",
"version": "0.51.1",
"version": "0.51.2",
"private": true,
"type": "module",
"scripts": {

View File

@@ -11,7 +11,8 @@ import {
listChapters,
getChapter,
getChapterPages,
createChapter
createChapter,
chapterLabel
} from './chapters';
function ok(body: unknown): Response {
@@ -129,6 +130,18 @@ describe('chapters api client', () => {
}
});
describe('chapterLabel', () => {
it('returns the site title verbatim when present', () => {
expect(chapterLabel({ number: 7, title: 'Ch.7 : Official' })).toBe(
'Ch.7 : Official'
);
});
it('falls back to "Chapter {number}" when title is null', () => {
expect(chapterLabel({ number: 3, title: null })).toBe('Chapter 3');
});
});
it('getChapterPages unwraps the {pages} envelope into the array', async () => {
fetchSpy.mockResolvedValueOnce(
ok({

View File

@@ -14,6 +14,10 @@ export type ChaptersPage = {
page: Page;
};
export function chapterLabel(c: Pick<Chapter, 'number' | 'title'>): string {
return c.title ?? `Chapter ${c.number}`;
}
export type ListOptions = {
limit?: number;
offset?: number;

View File

@@ -10,6 +10,7 @@
type TagRef
} from '$lib/api/mangas';
import { resyncManga } from '$lib/api/admin';
import { chapterLabel } from '$lib/api/chapters';
import { listTags, type Tag } from '$lib/api/tags';
import { session } from '$lib/session.svelte';
import Chip from '$lib/components/Chip.svelte';
@@ -45,6 +46,11 @@
continueChapter?.number ?? readProgress?.chapter_number ?? null
);
const continueChapterTitle = $derived(continueChapter?.title ?? null);
const continueLabel = $derived(
continueChapterNumber != null
? chapterLabel({ number: continueChapterNumber, title: continueChapterTitle })
: null
);
const authors = $derived<AuthorRef[]>(manga.authors);
const genres = $derived<GenreRef[]>(manga.genres);
@@ -431,7 +437,7 @@
>
<span class="continue-label">Continue reading</span>
<span class="continue-target">
Chapter {continueChapterNumber}{#if continueChapterTitle}: {continueChapterTitle}{/if}
{continueLabel}
{#if readProgress && readProgress.page > 1}
— page {readProgress.page}
{/if}
@@ -445,7 +451,7 @@
{#each chapters as c (c.id)}
<li>
<a href="/manga/{manga.id}/chapter/{c.id}">
Chapter {c.number}{#if c.title}: {c.title}{/if}
{chapterLabel(c)}
</a>
<span class="pages">({c.page_count} pages)</span>
</li>

View File

@@ -5,6 +5,7 @@
import { GAP_PX, type ReaderPageGap } from '$lib/api/preferences';
import { preferences } from '$lib/preferences.svelte';
import { updateReadProgress } from '$lib/api/read_progress';
import { chapterLabel } from '$lib/api/chapters';
import { resyncChapter } from '$lib/api/admin';
import { readerFullscreen } from '$lib/reader-fullscreen.svelte';
import { session } from '$lib/session.svelte';
@@ -28,9 +29,7 @@
const gapPx = $derived(GAP_PX[preferences.readerPageGap]);
const pageTitle = $derived(
chapter.title
? `Mangalord | ${manga.title} · Ch. ${chapter.number}: ${chapter.title}`
: `Mangalord | ${manga.title} · Ch. ${chapter.number}`
`Mangalord | ${manga.title} · ${chapterLabel(chapter)}`
);
// Prev/next chapter computed from the chapter list. listChapters
@@ -474,7 +473,7 @@
>
{#each sortedChapters as c (c.id)}
<option value={c.id}>
Ch. {c.number}{c.title ? ` ${c.title}` : ''}
{chapterLabel(c)}
</option>
{/each}
</select>
@@ -685,7 +684,7 @@
</span>
</button>
<span class="chapter-bar-current" aria-hidden="true">
Ch. {chapter.number}{#if chapter.title} {chapter.title}{/if}
{chapterLabel(chapter)}
</span>
<button
type="button"

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { fileUrl } from '$lib/api/client';
import { chapterLabel } from '$lib/api/chapters';
import { clearReadProgress, type ReadProgressSummary } from '$lib/api/read_progress';
import BookImage from '@lucide/svelte/icons/book-image';
import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -186,7 +187,7 @@
<a href="/manga/{u.manga_id}" class="title">{u.manga_title}</a>
<span class="target">
<a href="/manga/{u.manga_id}/chapter/{u.chapter.id}">
Chapter {u.chapter.number}{#if u.chapter.title}: {u.chapter.title}{/if}
{chapterLabel(u.chapter)}
</a>
<span class="muted">({u.chapter.page_count} pages)</span>
</span>