diff --git a/CLAUDE.md b/CLAUDE.md index e892a76..913a31a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,6 +46,8 @@ go fmt ./... go vet ./... ``` +**Note**: The project maintainer handles all Go compilation, testing, and error reporting. Claude Code should not run Go build commands or tests - any Go-related errors will be reported directly by the maintainer. + ### Frontend Assets (from views/ directory) ```bash cd views/ @@ -124,6 +126,8 @@ views/ │ ├── kategorie/ # Category pages │ ├── kontakt/ # Contact pages │ ├── ort/ # Places pages +│ ├── piece/ # Multi-issue piece pages +│ │ └── components/ # Piece-specific components (_piece_inhaltsverzeichnis, _piece_sequential_layout) │ ├── search/ # Search pages │ └── zitation/ # Citation pages ├── assets/ # Compiled output assets @@ -228,4 +232,210 @@ The application follows a **logic-in-Go, presentation-in-templates** approach: ### Adding New Template Logic 1. **First**: Add business logic to view models in Go 2. **Second**: Create reusable template helper functions if needed -3. **Last**: Use pre-processed data in templates for presentation only \ No newline at end of file +3. **Last**: Use pre-processed data in templates for presentation only + +## Multi-Issue Piece View (/beitrag/) + +The application supports viewing pieces/articles that span multiple issues through a dedicated piece view interface that aggregates content chronologically. + +### URL Structure & Routing + +**URL Pattern**: `/beitrag/:id` where ID format is `YYYY-NNN-PPP` (year-issue-page) +- **Example**: `/beitrag/1768-020-079` (piece starting at year 1768, issue 20, page 79) +- **Route Definition**: `PIECE_URL = "/beitrag/:id"` in `app/kgpz.go` +- **Controller**: `controllers.GetPiece(k.Library)` handles piece lookup and rendering + +### Architecture & Components + +**Controller** (`controllers/piece_controller.go`): +- Parses YYYY-NNN-PPP ID format using regex pattern matching +- Looks up pieces by year/issue/page when XML IDs aren't reliable +- Handles piece aggregation across multiple issues +- Returns 404 for invalid IDs or non-existent pieces + +**View Model** (`viewmodels/piece_view.go`): +- `PieceVM` struct aggregates data from multiple issues +- `AllIssueRefs []xmlmodels.IssueRef` - chronologically ordered issue references +- `AllPages []PiecePageEntry` - sequential page data with image paths +- Pre-processes page icons, grid layouts, and visibility flags +- Resolves image paths using registry system + +**Template System** (`views/routes/piece/`): +- `body.gohtml` - Two-column layout with Inhaltsverzeichnis and sequential pages +- `head.gohtml` - Page metadata and title generation +- `components/_piece_inhaltsverzeichnis.gohtml` - Table of contents with piece content +- `components/_piece_sequential_layout.gohtml` - Chronological page display + +### Key Features + +**Multi-Issue Aggregation**: +- Pieces spanning multiple issues are unified in a single view +- Chronological ordering preserves reading sequence across issue boundaries +- Issue context (year/number) displayed with each page for reference + +**Component Reuse**: +- Reuses `_inhaltsverzeichnis_eintrag` template for consistent content display +- Integrates with existing `_newspaper_layout` components for single-page viewer +- Shares highlighting system and navigation patterns with issue view + +**Sequential Layout**: +- Two-column responsive design: Inhaltsverzeichnis (1/3) + Page Layout (2/3) +- Left-aligned page indicators with format: `[icon] YYYY Nr. XX, PageNum` +- No grid constraints - simple sequential flow for multi-issue reading + +**Highlighting System Integration**: +- Uses same intersection observer system as issue view (`main.js`) +- Page links in Inhaltsverzeichnis turn red when corresponding page is visible +- Page indicators above images also highlight during scroll +- Automatic scroll-to-highlighted functionality + +### Template Integration + +**Helper Functions** (`templating/engine.go`): +- `GetPieceURL(year, issueNum, page int) string` - generates piece URLs +- Reuses existing `PageIcon()` for consistent icon display +- `getImagePathFromRegistry()` for proper image path resolution + +**Data Attributes for JavaScript**: +- `data-page-container` on page containers for scroll detection +- `data-page-number` on Inhaltsverzeichnis links for highlighting +- `newspaper-page-container` class for intersection observer +- `inhalts-entry` class for hover and highlighting behavior + +**Responsive Behavior**: +- Mobile: Single column with collapsible Inhaltsverzeichnis +- Desktop: Fixed two-column layout with sticky table of contents +- Single-page viewer integration with proper navigation buttons + +### Usage Examples + +**Linking to Pieces**: +```gohtml + + gesamten beitrag anzeigen + +``` + +**Page Navigation in Inhaltsverzeichnis**: +```gohtml + + {{ $issueRef.When.Day }}.{{ $issueRef.When.Month }}.{{ $issueRef.When.Year }} [Nr. {{ $pageEntry.IssueNumber }}], {{ $pageEntry.PageNumber }} + +``` + +### Error Handling + +**Invalid IDs**: Returns 404 for malformed YYYY-NNN-PPP format +**Missing Pieces**: Returns 404 when piece lookup fails in XML data +**Missing Images**: Graceful fallback with "Keine Bilder verfügbar" message +**Cross-Issue Navigation**: Handles pieces spanning non-consecutive issues + +## Direct Page Navigation System + +The application provides a direct page navigation system that allows users to jump directly to any page by specifying year and page number, regardless of which issue contains that page. + +### URL Structure + +**New URL Format**: All page links now use path parameters instead of hash fragments: +- **Before**: `/1771/42#page-166` +- **After**: `/1771/42/166` + +This change applies to all page links throughout the application, including: +- Page sharing/citation links +- Inhaltsverzeichnis page navigation +- Single page viewer navigation + +### Page Jump Interface + +**Location**: Available on year overview pages (`/jahrgang/:year`) + +**Features**: +- **Year Selection**: Dropdown with all available years (1764-1779) +- **Page Input**: Numeric input with validation +- **HTMX Integration**: Real-time error feedback without page reload +- **Auto-redirect**: Successful lookups redirect to `/year/issue/page` + +**URL Patterns**: +- **Form Submission**: `POST /jump` with form data +- **Direct URL**: `GET /jump/:year/:page` (redirects to found issue) + +### Error Handling + +**Comprehensive Validation**: +- **Invalid Year**: Years outside 1764-1779 range +- **Invalid Page**: Non-numeric or negative page numbers +- **Page Not Found**: Page doesn't exist in any issue of specified year +- **Form Preservation**: Error responses maintain user input for correction + +**HTMX Error Responses**: +- Form replaced with error version showing red borders and error messages +- Specific error targeting (year field vs. page field) +- Graceful degradation with clear user feedback + +### Auto-Scroll Implementation + +**URL-Based Navigation**: +- Pages accessed via `/year/issue/page` auto-scroll to target page +- JavaScript detects path-based page numbers (not hash fragments) +- Smooth scrolling with proper timing for layout initialization +- Automatic highlighting in Inhaltsverzeichnis + +**Technical Implementation**: +```javascript +// Auto-scroll on page load if targetPage is specified +const pathParts = window.location.pathname.split('/'); +if (pathParts.length >= 4 && !isNaN(pathParts[pathParts.length - 1])) { + const pageNumber = pathParts[pathParts.length - 1]; + // Scroll to page container and highlight +} +``` + +### Controller Architecture + +**Page Jump Controller** (`controllers/page_jump_controller.go`): +- `FindIssueByYearAndPage()` - Lookup function for issue containing specific page +- `GetPageJump()` - Handles direct URL navigation (`/jump/:year/:page`) +- `GetPageJumpForm()` - Handles form submissions (`POST /jump`) +- Error rendering with HTML form generation + +**Issue Controller Updates** (`controllers/ausgabe_controller.go`): +- Enhanced to handle optional page parameter in `/:year/:issue/:page?` +- Page validation against issue page ranges +- Target page passed to template for auto-scroll JavaScript + +### Link Generation Updates + +**JavaScript Functions** (`views/transform/main.js`): +- `copyPagePermalink()` - Generates `/year/issue/page` URLs +- `generatePageCitation()` - Uses new URL format for citations +- `scrollToPageFromURL()` - URL-based navigation (replaces hash-based) + +**Template Integration**: +- Page links updated throughout templates to use new URL format +- Maintains backward compatibility for beilage/supplement pages (still uses hash) +- HTMX navigation preserved with new URL structure + +### Usage Examples + +**Direct Page Access**: +``` +http://127.0.0.1:8080/1771/42/166 # Direct link to page 166 +``` + +**Page Jump Form**: +```html +
+``` + +**Link Generation**: +```javascript +// New format for regular pages +const pageUrl = `/${year}/${issue}/${pageNumber}`; +// Old format still used for beilage pages +const beilageUrl = `${window.location.pathname}#beilage-1-page-${pageNumber}`; +``` \ No newline at end of file diff --git a/app/kgpz.go b/app/kgpz.go index b053876..c70c98c 100644 --- a/app/kgpz.go +++ b/app/kgpz.go @@ -37,6 +37,10 @@ const ( AGENTS_OVERVIEW_URL = "/akteure/:letterorid" CATEGORY_OVERVIEW_URL = "/kategorie/:category" + PIECE_URL = "/beitrag/:id" + PIECE_PAGE_URL = "/beitrag/:id/:page" + PAGE_JUMP_URL = "/jump/:year/:page" + PAGE_JUMP_FORM_URL = "/jump" ISSSUE_URL = "/:year/:issue/:page?" ADDITIONS_URL = "/:year/:issue/beilage/:page?" ) @@ -147,6 +151,12 @@ func (k *KGPZ) Routes(srv *fiber.App) error { srv.Get(PLACE_OVERVIEW_URL, controllers.GetPlace(k.Library)) srv.Get(CATEGORY_OVERVIEW_URL, controllers.GetCategory(k.Library)) srv.Get(AGENTS_OVERVIEW_URL, controllers.GetAgents(k.Library)) + srv.Get(PIECE_PAGE_URL, controllers.GetPieceWithPage(k.Library)) + srv.Get(PIECE_URL, controllers.GetPiece(k.Library)) + + // Page jump routes for direct navigation + srv.Get(PAGE_JUMP_URL, controllers.GetPageJump(k.Library)) + srv.Post(PAGE_JUMP_FORM_URL, controllers.GetPageJumpForm(k.Library)) // TODO: YEAR_OVERVIEW_URL being /:year is a bad idea, since it captures basically everything, // probably creating problems with static files, and also in case we add a front page later. diff --git a/controllers/ausgabe_controller.go b/controllers/ausgabe_controller.go index 19744b9..97afaad 100644 --- a/controllers/ausgabe_controller.go +++ b/controllers/ausgabe_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "fmt" "strconv" "github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging" @@ -30,6 +31,18 @@ func GetIssue(kgpz *xmlmodels.Library) fiber.Handler { return c.SendStatus(fiber.StatusNotFound) } + // Handle optional page parameter + pageParam := c.Params("page") + var targetPage int + if pageParam != "" { + pi, err := strconv.Atoi(pageParam) + if err != nil || pi < 1 { + logging.Error(err, "Page is not a valid number") + return c.SendStatus(fiber.StatusNotFound) + } + targetPage = pi + } + issue, err := viewmodels.NewSingleIssueView(yi, di, kgpz) if err != nil { @@ -37,6 +50,14 @@ func GetIssue(kgpz *xmlmodels.Library) fiber.Handler { return c.SendStatus(fiber.StatusNotFound) } - return c.Render("/ausgabe/", fiber.Map{"model": issue, "year": yi, "issue": di}, "fullwidth") + // If a page was specified, validate it exists in this issue + if targetPage > 0 { + if targetPage < issue.Issue.Von || targetPage > issue.Issue.Bis { + logging.Debug(fmt.Sprintf("Page %d not found in issue %d/%d (range: %d-%d)", targetPage, yi, di, issue.Issue.Von, issue.Issue.Bis)) + return c.SendStatus(fiber.StatusNotFound) + } + } + + return c.Render("/ausgabe/", fiber.Map{"model": issue, "year": yi, "issue": di, "targetPage": targetPage}, "fullwidth") } } diff --git a/controllers/page_jump_controller.go b/controllers/page_jump_controller.go new file mode 100644 index 0000000..7e78d58 --- /dev/null +++ b/controllers/page_jump_controller.go @@ -0,0 +1,227 @@ +package controllers + +import ( + "fmt" + "strconv" + + "github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging" + "github.com/Theodor-Springmann-Stiftung/kgpz_web/xmlmodels" + "github.com/gofiber/fiber/v2" +) + +// FindIssueByYearAndPage finds the first issue in a given year that contains the specified page +func FindIssueByYearAndPage(year, page int, library *xmlmodels.Library) (*xmlmodels.Issue, error) { + library.Issues.Lock() + defer library.Issues.Unlock() + + var foundIssues []xmlmodels.Issue + + // Find all issues in the given year that contain the page + for _, issue := range library.Issues.Array { + if issue.Datum.When.Year == year && page >= issue.Von && page <= issue.Bis { + foundIssues = append(foundIssues, issue) + } + } + + if len(foundIssues) == 0 { + return nil, fmt.Errorf("no issue found containing page %d in year %d", page, year) + } + + // Return the first issue chronologically (by issue number) + firstIssue := foundIssues[0] + for _, issue := range foundIssues { + if issue.Number.No < firstIssue.Number.No { + firstIssue = issue + } + } + + return &firstIssue, nil +} + +func GetPageJump(kgpz *xmlmodels.Library) fiber.Handler { + return func(c *fiber.Ctx) error { + // Parse year parameter + yearStr := c.Params("year") + year, err := strconv.Atoi(yearStr) + if err != nil || year < MINYEAR || year > MAXYEAR { + logging.Debug("Invalid year for page jump: " + yearStr) + return c.Status(fiber.StatusBadRequest).SendString(` +