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

2
backend/Cargo.lock generated
View File

@@ -1470,7 +1470,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mangalord" name = "mangalord"
version = "0.51.1" version = "0.51.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "mangalord" name = "mangalord"
version = "0.51.1" version = "0.51.2"
edition = "2021" edition = "2021"
default-run = "mangalord" default-run = "mangalord"

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-title')).toHaveText('Berserk');
await expect(page.getByTestId('manga-author')).toContainText('Kentaro Miura'); await expect(page.getByTestId('manga-author')).toContainText('Kentaro Miura');
await expect(page.getByTestId('manga-cover')).toBeVisible(); 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(); await expect(page.getByTestId('bookmark-signin')).toBeVisible();
}); });

View File

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

View File

@@ -11,7 +11,8 @@ import {
listChapters, listChapters,
getChapter, getChapter,
getChapterPages, getChapterPages,
createChapter createChapter,
chapterLabel
} from './chapters'; } from './chapters';
function ok(body: unknown): Response { 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 () => { it('getChapterPages unwraps the {pages} envelope into the array', async () => {
fetchSpy.mockResolvedValueOnce( fetchSpy.mockResolvedValueOnce(
ok({ ok({

View File

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

View File

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

View File

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

View File

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