Add comprehensive planning document for displaying new ComicInfo.xml metadata fields on the book detail page. New fields to be added: - Reading direction (RTL/LTR/vertical) badge - Community rating display (0-10 scale) - Universal fields: series count, volume, imprint, age rating - Comic-specific: manga type, story arc, scan info, B&W flag Implementation approach: - SSR-first rendering (no client-side fetching) - TailwindCSS only (no custom CSS) - Conditional display based on field validity - Follows existing template patterns This document provides step-by-step implementation guidance with code examples and testing scenarios for the frontend team. Related: IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md
16 KiB
Frontend Implementation Plan: Add Comic Metadata Fields to Book Detail Page
Status: Planning Document (Not Implemented) Date: March 30, 2026 Scope: Display new comic/manga metadata fields on the book detail page
Overview
This document outlines how to add the new ComicInfo.xml metadata fields to the book detail page frontend. These fields are already added to the database schema and backend models (per IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md).
New Fields to Display
Universal Fields (apply to all formats: ebooks, audiobooks, comics)
series_count- Total items in seriesvolume- Volume/omnibus numberimprint- Publisher imprint (e.g., Vertigo, HarperCollinsEpic)age_rating- Age rating: Everyone, Teen, Mature, Adultweb_url- URL to info page (Goodreads, ComicVine, MangaUpdates, etc.)metadata_notes- Notes from metadata files (ComicInfo.xml, EPUB, PDF)community_rating- Pre-existing community rating (0.0-10.0) - distinct from user ratings
Comic-Specific Fields
manga_type- Raw Manga field: unknown, no, yes, yes_and_right_to_leftreading_direction- Computed reading direction: auto, ltr, rtl, verticalstory_arc- Story arc name (e.g., "The Dark Phoenix Saga", "Civil War")is_black_and_white- Black and white flagalternate_info- JSONB: {alternate_series, alternate_number, alternate_count}scan_information- Scan information (scanner group, resolution, etc.)summary- Summary from ComicInfo.xml (may merge with description)
Current Book Detail Page Structure
File: templates/book_detail.templ
Current Layout Sections:
-
Top Section (lines 23-130)
- Cover image (left)
- Title, author, action buttons (right)
- Rating display (user ratings)
- Series badge
- Description/Synopsis
-
Progress Section (lines 132-185)
- Reading progress bar
- Progress stats grid
-
Metadata Grid (lines 186-253)
- Publication info (Publisher, Published, ISBN, Language, Edition, Pages, Genre, Copyright Year)
- Technical info (Format, File Size)
- External links section
-
Collections Section (lines 347-367)
Implementation Plan
Step 1: Add Reading Direction Badge (Display Priority: HIGH)
Location: After Series Badge (line 117)
Rationale: Reading direction is critical for manga/comic readers. Display prominently like the series badge.
<!-- Reading Direction Badge (for manga/comics) -->
if book.ReadingDirection.Valid && book.ReadingDirection.String != "" && book.ReadingDirection.String != "auto" {
<div class="mb-4">
<span
class="px-3 py-1 rounded-full text-sm font-semibold"
style="background-color: var(--accent); color: white;"
>
📖 { strings.ToUpper(book.ReadingDirection.String) }
</span>
</div>
}
Styling Notes:
- Use
var(--accent)for consistency with series badge - Only show if not "auto" (i.e., explicitly set to ltr, rtl, or vertical)
- Icon: 📖 for visual clarity
Step 2: Add Community Rating Display (Display Priority: HIGH)
Location: After User Rating Display (line 103)
Rationale: Community rating complements user rating. Show side-by-side for comparison.
<!-- Community Rating Display (from metadata) -->
if book.CommunityRating.Valid && book.CommunityRating.Float64 > 0 {
<div class="mb-2">
<span class="text-lg" style="color: var(--text-secondary);">
Community Rating:
<span class="font-bold" style="color: var(--text-primary);">
@templ.Raw(renderStars(getBookRating(int(book.CommunityRating.Float64 * 2))))
</span>
<span class="ml-2 text-sm" style="color: var(--text-secondary);">
({ fmt.Sprintf("%.1f", book.CommunityRating.Float64) } / 10)
</span>
</span>
</div>
}
Styling Notes:
- Smaller text than user rating (text-lg vs text-2xl)
- Gray/subtle color to distinguish from user rating
- Scale: Community rating is 0-10, user rating display is 0-5 stars
- Conversion:
community_rating * 2to get 0-10 scale for star display
- Conversion:
Step 3: Add Comic-Specific Badges (Display Priority: MEDIUM)
Location: After Reading Direction Badge (line 117)
Rationale: Group all manga/comic-specific badges together for easy scanning.
<!-- Comic-Specific Badges -->
<div class="flex flex-wrap gap-2 mb-4">
<!-- Age Rating Badge -->
if book.AgeRating.Valid && book.AgeRating.String != "" {
<span
class="px-2 py-1 rounded-full text-xs font-semibold"
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
>
{ book.AgeRating.String }
</span>
}
<!-- Black and White Badge -->
if book.IsBlackAndWhite.Valid && book.IsBlackAndWhite.Bool {
<span
class="px-2 py-1 rounded-full text-xs font-semibold"
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
>
B&W
</span>
}
<!-- Story Arc Badge -->
if book.StoryArc.Valid && book.StoryArc.String != "" {
<span
class="px-2 py-1 rounded-full text-xs font-semibold"
style="background-color: var(--bg-primary); border: 1px solid var(--border); color: var(--text-secondary);"
>
📚 { book.StoryArc.String }
</span>
}
</div>
Styling Notes:
- Use subtle styling (smaller, outlined) to differentiate from primary badges
- Flex wrap for responsive layout
- Icons: 📚 for story arc
Step 4: Add Universal Series Info to Metadata Grid (Display Priority: HIGH)
Location: In Metadata Grid section, after Series/Number fields
Note: The current template doesn't display Series/Number in the metadata grid (only as badge). We'll add all series-related fields together.
Insertion Point: After Genre field (line 235)
<!-- Series Information (if not shown in badge) -->
if book.SeriesCount.Valid && book.SeriesCount.Int32 > 0 {
<div>
<p style="color: var(--text-secondary)">Series Count</p>
<p>{ book.SeriesCount.Int32 } items</p>
</div>
}
if book.Volume.Valid && book.Volume.Int32 > 0 {
<div>
<p style="color: var(--text-secondary)">Volume</p>
<p>Vol. { book.Volume.Int32 }</p>
</div>
}
if book.Imprint.Valid && book.Imprint.String != "" {
<div>
<p style="color: var(--text-secondary)">Imprint</p>
<p>{ book.Imprint.String }</p>
</div>
}
Step 5: Add Comic-Specific Metadata to Grid (Display Priority: MEDIUM)
Location: In Metadata Grid section, after Copyright Year (line 241)
<!-- Comic-Specific Fields -->
if book.MangaType.Valid && book.MangaType.String != "" && book.MangaType.String != "unknown" {
<div>
<p style="color: var(--text-secondary)">Manga Type</p>
<p class="capitalize">{ strings.ReplaceAll(book.MangaType.String, "_", " ") }</p>
</div>
}
if book.ScanInformation.Valid && book.ScanInformation.String != "" {
<div>
<p style="color: var(--text-secondary)">Scan Info</p>
<p class="text-sm" style="color: var(--text-secondary);">{ book.ScanInformation.String }</p>
</div>
}
if book.AlternateInfo.Valid && len(book.AlternateInfo.Bytes) > 0 {
<div>
<p style="color: var(--text-secondary)">Alternate Series</p>
<p class="text-sm">{ string(book.AlternateInfo.Bytes) }</p>
</div>
}
Note for AlternateInfo: This is JSONB data stored as []byte. You may want to:
- Parse the JSON and display formatted
- Or create a helper function to extract specific fields
Helper function example (add to templates/helpers.go):
// getAlternateSeries extracts the alternate series name from JSONB data
func getAlternateSeries(data pgtype.Bytes) string {
if !data.Valid || len(data.Bytes) == 0 {
return ""
}
var result map[string]interface{}
if err := json.Unmarshal(data.Bytes, &result); err != nil {
return ""
}
if series, ok := result["alternate_series"].(string); ok {
return series
}
return ""
}
Then in template:
if altSeries := getAlternateSeries(book.AlternateInfo); altSeries != "" {
<div>
<p style="color: var(--text-secondary)">Alternate Series</p>
<p>{ altSeries }</p>
</div>
}
Step 6: Add Summary Section (Display Priority: MEDIUM)
Location: After Description/Synopsis section (line 128)
Rationale: Summary from ComicInfo.xml may differ from Calibre description. Show both if present.
<!-- Summary (from ComicInfo.xml, if different from description) -->
if book.Summary.Valid && book.Summary.String != "" && book.Summary.String != book.Description.String {
<div class="mb-6">
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Comic Summary</h3>
<div class="max-h-[20rem] overflow-y-auto pr-2" style="color: var(--text-secondary)">
@UnsafeHTML(
bluemonday.UGCPolicy().Sanitize(book.Summary.String),
).ToComponent()
</div>
</div>
}
Step 7: Add Metadata Notes Section (Display Priority: LOW)
Location: After Summary section (or at the bottom before Collections)
Rationale: Metadata notes are technical/scanner information. Less important for end users.
<!-- Metadata Notes (technical notes from metadata files) -->
if book.MetadataNotes.Valid && book.MetadataNotes.String != "" {
<div
class="card p-6 rounded-lg border mb-6"
style="background-color: var(--bg-secondary); border-color: var(--border);"
>
<h3 class="font-semibold mb-2" style="color: var(--text-primary)">Metadata Notes</h3>
<div class="text-sm" style="color: var(--text-secondary);">
{ book.MetadataNotes.String }
</div>
</div>
}
Step 8: Add Web URL Link (Display Priority: MEDIUM)
Location: In External Links section (line 258)
Rationale: Add web_url as another external link alongside Goodreads, OpenLibrary, etc.
<!-- In the External Links flex container -->
if book.WebUrl.Valid && book.WebUrl.String != "" {
<a
href={ book.WebUrl.String }
target="_blank"
rel="noopener noreferrer"
class="text-sm hover:underline flex items-center gap-1"
style="color: var(--accent);"
>
🔗 { getDomainName(book.WebUrl.String) }
</a>
}
Helper function to extract domain name (add to templates/helpers.go):
// getDomainName extracts a clean domain name from URL for display
func getDomainName(rawURL string) string {
if rawURL == "" {
return "Source"
}
u, err := url.Parse(rawURL)
if err != nil {
return "Source"
}
// Return hostname without www.
hostname := u.Hostname()
return strings.TrimPrefix(hostname, "www.")
}
TypeScript Updates (No Changes Required)
File: web/src/book-detail.ts
Status: No changes needed. This file only handles Alpine.js modal visibility functions.
Note: The book detail page uses SSR (server-side rendering) exclusively. No client-side data fetching or Alpine.js data manipulation is needed for these new fields.
Template Helper Functions Needed
Add these functions to templates/helpers.go (or existing helpers file):
package templates
import (
"encoding/json"
"net/url"
"strings"
"github.com/jackc/pgx/v5/pgtype"
)
// getAlternateSeries extracts the alternate series name from JSONB data
func getAlternateSeries(data pgtype.Bytes) string {
if !data.Valid || len(data.Bytes) == 0 {
return ""
}
var result map[string]interface{}
if err := json.Unmarshal(data.Bytes, &result); err != nil {
return ""
}
if series, ok := result["alternate_series"].(string); ok {
return series
}
return ""
}
// getDomainName extracts a clean domain name from URL for display
func getDomainName(rawURL string) string {
if rawURL == "" {
return "Source"
}
u, err := url.Parse(rawURL)
if err != nil {
return "Source"
}
hostname := u.Hostname()
return strings.TrimPrefix(hostname, "www.")
}
// formatAlternateInfo formats alternate series info as readable string
func formatAlternateInfo(data pgtype.Bytes) string {
if !data.Valid || len(data.Bytes) == 0 {
return ""
}
var result map[string]interface{}
if err := json.Unmarshal(data.Bytes, &result); err != nil {
return ""
}
var parts []string
if series, ok := result["alternate_series"].(string); ok && series != "" {
parts = append(parts, series)
}
if num, ok := result["alternate_number"].(float64); ok && num > 0 {
parts = append(parts, fmt.Sprintf("#%d", int(num)))
}
if count, ok := result["alternate_count"].(float64); ok && count > 0 {
parts = append(parts, fmt.Sprintf("(of %d)", int(count)))
}
return strings.Join(parts, " ")
}
Implementation Checklist
- Step 1: Add Reading Direction Badge (after line 117)
- Step 2: Add Community Rating Display (after line 103)
- Step 3: Add Comic-Specific Badges (after line 117)
- Step 4: Add Universal Series Info to Metadata Grid (after line 235)
- Step 5: Add Comic-Specific Metadata to Grid (after line 241)
- Step 6: Add Summary Section (after line 128)
- Step 7: Add Metadata Notes Section (before Collections)
- Step 8: Add Web URL Link (in External Links section)
- Add template helper functions to
templates/helpers.go - Test with manga book (RTL reading direction)
- Test with Western comic (LTR reading direction)
- Test with webtoon (vertical reading direction)
- Test with ebook (no comic metadata)
- Verify responsive layout on mobile/desktop
- Verify dark/light theme compatibility
Testing Scenarios
Test Case 1: Japanese Manga
Expected Display:
- Reading Direction: "RTL" badge
- Manga Type: "yes_and_right_to_left" in metadata grid
- Community Rating: displayed alongside user rating
Test Case 2: Western Comic
Expected Display:
- Reading Direction: "LTR" badge (or hidden if auto)
- Age Rating: "Teen" or "Mature" badge
- Story Arc: displayed if present
- Publisher/Imprint: displayed in metadata grid
Test Case 3: Webtoon/Manhwa
Expected Display:
- Reading Direction: "vertical" badge
- Language: "ko" (Korean) in metadata grid
- Format-specific metadata displayed
Test Case 4: Regular Ebook
Expected Display:
- No reading direction badge (or "auto" hidden)
- No comic-specific fields
- Only universal fields (series count, volume, imprint, age rating) if present
Design Principles Followed
- ✅ SSR-First: All data server-side rendered, no client-side fetching
- ✅ Progressive Enhancement: Page works without JavaScript
- ✅ TailwindCSS Only: No custom CSS added
- ✅ Existing Patterns: Follows current badge/grid styling
- ✅ Responsive: Uses existing responsive grid classes
- ✅ Theme-Aware: Uses CSS variables (
var(--accent),var(--text-secondary), etc.) - ✅ Accessible: Semantic HTML, proper contrast
- ✅ Conditional Rendering: Only show fields if they have values
- ✅ No Backend Changes: Frontend-only, uses existing API/handler types
Related Documentation
IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md- Full backend implementationPROJECT_GUIDELINES.md- Project coding standards and conventionstemplates/book_detail.templ- Current book detail templateinternal/handlers/media_detail.go- MediaDetail struct definition
Notes
- No TypeScript changes needed: All data is SSR'd from Go handlers
- No API changes needed: MediaDetail already embeds database.MediaItems with new fields
- Template helpers: May need helper functions for JSONB parsing and URL display
- Styling consistency: All new elements use existing CSS variables and Tailwind classes
- Conditional display: Most fields should only display if they have values (check
.Valid)
End of Document