Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Continue Working widget shows pixelated thumbnails

Open
#536 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
javascript
Domain
frontend

Research direction

Start in components/continue-working/index.js at getProjectThumbnail() and review how IIIF canvas thumbnails and image services are resolved for Presentation 2 and 3. Run jekyll s, sign in, and visit / to verify crisp ~200 CSS px thumbnails, 400 px IIIF requests, and no malformed URL 404s in the Network panel.

Written by the indexing model from the issue text.

Description

bug component

Summary

The "Continue Working" widget on the home page renders project thumbnails at very low resolution. Images appear visibly pixelated, especially on HiDPI displays.

Reproduction

  1. Sign in and visit the home page (/).
  2. Observe the project thumbnails in the "Continue Working" card.
  3. Thumbnails render blurry/pixelated rather than crisp at the ~200 CSS px display size.

Reproduced against the Italian Paleography manuscript hosted on iiif.library.utoronto.ca (IIIF Image API 2 / Presentation API 2.1).

Root cause

In components/continue-working/index.js → getProjectThumbnail(), two stacked bugs combine to deliver a tiny image:

Bug 1 — canvas.thumbnail trusted blindly without checking declared size

Line 194:

let thumbnailUrl = canvas.thumbnail?.id ?? canvas.thumbnail?.['@id'] ?? canvas.thumbnail
if (!thumbnailUrl) {
    // …construct a sized URL from the IIIF service…
}

For the Toronto manifest the canvas advertises:

"thumbnail": {
  "@id": ".../full/80,/0/default.jpg",
  "width": 80, "height": 120
}

So thumbnailUrl is set to an 80-pixel-wide image and the entire fallback path is skipped. The widget displays it in a flex: 1 1 200px; aspect-ratio: 1; object-fit: cover section → stretched from 80 → ~200 CSS px (2.5×, worse on HiDPI). That's the pixelation.

The IIIF Presentation spec lets canvas.thumbnail be any size — it's metadata, not a sizing constraint. Trusting it without checking declared width/height is the bug.

Bug 2 — Latent: malformed URLs in the fallback path

Even if canvas.thumbnail were missing, the constructed fallback URLs are malformed. Lines 209–213 build:

`${baseUrl}/full/${isV3 ? 'max' : 'full'}/200,/0/default.jpg`

That produces e.g. …/full/full/200,/0/default.jpg — three size-related segments where IIIF wants region/size/rotation. Verified against the Toronto server:

GET …/full/full/200,/0/default.jpg → 404
GET …/full/200,/0/default.jpg      → 200

The ${isV3 ? 'max' : 'full'}/ insert stuffs a size token in front of the actual size 200,, pushing rotation/quality into invalid path positions. Both the square and full attempts 404, hitting the final .catch(() => thumbnailUrl = imageUrl) — which on Toronto is …/full/512,/0/default.jpg (still small).

Bug 3 — Latent: thumbnail as array not handled

IIIF Presentation 3.0 §5.3.1 requires thumbnail to be an array of objects. The current canvas.thumbnail?.id ?? canvas.thumbnail?.['@id'] chain returns undefined for arrays and falls through to canvas.thumbnail itself (the array) as the URL — which renders as [object Object] in the <img src>.

Suggested fix

Always derive a sized thumbnail from the IIIF Image service when one is available; only fall back to canvas.thumbnail when its declared width is sufficient; correctly construct the IIIF URL.

async getProjectThumbnail(project, annotationPageId) {
    try {
        if (!annotationPageId) return this.generateProjectPlaceholder(project)
        const annotationPage = await vault.get(
            `${TPEN.servicesURL}/project/${project._id}/page/${annotationPageId}`,
            'annotationpage', true
        )
        if (!annotationPage?.target) return this.generateProjectPlaceholder(project)
        const canvas = await vault.getWithFallback(annotationPage.target, 'canvas', project.manifest)
        if (!canvas) return this.generateProjectPlaceholder(project)

        const isV3 = Array.isArray(canvas.items) || canvas.type === "Canvas"
        const annotation = isV3 ? canvas.items?.[0]?.items?.[0] : canvas.images?.[0]
        const rawService = isV3 ? annotation?.body?.service : annotation?.resource?.service
        const service = Array.isArray(rawService) ? rawService[0] : rawService
        const serviceBase = service?.id ?? service?.['@id']

        // Prefer service-derived sized thumbnail (sharp at ~200 CSS px on HiDPI)
        if (serviceBase) {
            const base = serviceBase.replace(/\/$/, '')
            return `${base}/full/400,/0/default.jpg`
        }

        // canvas.thumbnail can be array (P3) or object/string (P2/P3)
        const t = Array.isArray(canvas.thumbnail) ? canvas.thumbnail[0] : canvas.thumbnail
        const tWidth = t?.width ?? 0
        if (tWidth >= 200) {
            return t?.id ?? t?.['@id'] ?? (typeof t === 'string' ? t : null) ?? this.generateProjectPlaceholder(project)
        }

        // Last resort — the painting body's image URL
        const imageUrl = isV3
            ? annotation?.body?.id ?? annotation?.body?.['@id']
            : annotation?.resource?.['@id'] ?? annotation?.resource?.id
        return imageUrl ?? this.generateProjectPlaceholder(project)
    } catch (error) {
        console.error('Error getting thumbnail:', error)
        return this.generateProjectPlaceholder(project)
    }
}

What this changes:

  • Always asks the IIIF service for a 400 px-wide rendition first (sharp on HiDPI 2× displays at 200 CSS px).
  • Only uses canvas.thumbnail when no service is available and the declared thumbnail is at least 200 wide.
  • Drops the broken square/max-prefix attempt chain.
  • Handles thumbnail as an array (Presentation 3.0 shape).

Verification

  1. jekyll s, sign in, visit /.
  2. Confirm "Continue Working" thumbnails render crisply (no visible pixelation) at the ~200 CSS px display size.
  3. Confirm Network panel shows …/full/400,/0/default.jpg requests, no 404s on …/full/full/200,/… or …/square/full/200,/….
Dominant language
JavaScript
Stars
2
Forks
3
PR merge metrics
No merged PRs in 30d

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from CenterForDigitalHumanities/TPEN-interfaces

All issues in CenterForDigitalHumanities/TPEN-interfaces

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.