Linked-materials hover badge: show what the material is (type, size, PDF page count), not just how many #57

Closed
opened 2026-07-30 08:53:56 +00:00 by laoc · 1 comment
Owner

The request

On a resource card, when material is linked, a badge appears over the cover on hover: "📎 1 Materialien verlinkt". A user asked whether the hover could say a little more about what is behind the card — e.g. "a linked PDF with 12 pages", "an image", "a PowerPoint with 24 slides" — rather than only how many things there are.

linked-materials badge

(card: "Anders sein" heißt einmalig sein…, rpi-ekkw-ekhn.de).

Where this lives today

src/lib/components/educational/AMBResourceCard.svelte:388-395 — the badge is already hover-only (opacity-0 … group-hover:opacity-100), so there is a place to put this; it just has nothing but a count in it.

The count comes from :74-78:

const linkedMaterialsCount = $derived(
  (resource?.tags ?? []).filter((t) => t[0] === 'encoding:contentUrl' || t[0] === 'r').length
);

Note it merges two different things: uploaded/attached files (encoding:contentUrl) and external references (r). Those have very different metadata available, which shapes what is feasible below.

What is already available — no new data needed

getAMBEncodings() (src/lib/helpers/educational/ambHelpers.js:330-351) already returns per attached file:

{ url, mimeType, size, sha256, name }

mimeType comes from encoding:encodingFormat and size from encoding:contentSize, both emitted at publish time (src/lib/helpers/publication/publicationTags.js:127-131).

So "a linked PDF (2.4 MB)", "an image", "a PowerPoint" is achievable today from data already on the event. That is most of the user's request and it needs no format change, no migration, and no network call. Good first slice.

What is not available — the page/slide counts

AMB's encoding:* has no page or slide count, so nothing on the event carries it. Options, roughly in cost order:

PDFs are nearly free. src/lib/server/pdfThumbnail.js:27 already does:

const doc = await pdfjs.getDocument({ data, isEvalSupported: false }).promise;

doc.numPages is right there. The /api/pdf-thumbnail endpoint already fetches, renders and caches the file, and pdfThumbnailGate.js already implements the rights policy for when the app may touch it at all (open license, or an attested Blossom upload with a sha256). Returning a page count alongside the thumbnail — or from a sibling endpoint sharing the cache — reuses all of that. This is the cheap path and I would do it first.

Slide counts are not. There is no PPTX handling anywhere in the app today. It would mean a new server-side dependency to unzip the OOXML and count ppt/slides/slideN.xml, plus the same rights gate. Real work, and worth deciding whether it earns its keep versus just saying "a PowerPoint".

Images could carry dimensions the same way, though nothing extracts them today either.

Three constraints worth knowing before scoping

  1. r tags carry no type at all — just a URL. For external links the best available guess is the file extension, or an HTTP HEAD for Content-Type (a network call per card, with the same rights and privacy questions as any outbound fetch). Deciding what an external link renders as is part of this issue, not an afterthought.

  2. Multi-file resources have a pairing problem. getAMBEncodings() aligns encoding:contentUrl, encoding:encodingFormat and encoding:contentSize by position across separate tag lists. The publish path notes this explicitly (publicationTags.js:122-124): "Single file by design — the positional getAMBEncodings pairing can't disambiguate mixed link/upload runs with heterogeneous optional fields." So if one of several files is missing a format or size, the indices shift and a later file gets labelled with an earlier file's type. A per-item hover list for multi-file resources needs that fixed — or needs to degrade to a summary when the tag counts do not line up.

  3. A long-tail question: what does the hover show for 5 linked items? A list will not fit in a badge. Probably "PDF · 12 pages, 2 images, 1 more" or similar — a design call, not just an engineering one.

Adjacent, small, and visible in the screenshot

The badge reads "1 Materialien verlinkt" — wrong plural. Same in English: "{count} linked materials" renders "1 linked materials". Neither messages/de.json:2742 nor messages/en.json:2742 has a plural form, and there are currently zero ICU plural forms anywhere in messages/de.json. Whoever touches this string should fix that at the same time; if this is the first plural in the project it is worth checking how the Paraglide setup wants them expressed.

Suggested slicing

  1. Type + size from data already on the event, uploads only. Fixes the plural while in there.
  2. PDF page count via the existing /api/pdf-thumbnail machinery and its rights gate.
  3. External r links — decide the policy, then implement.
  4. PPTX slide counts, only if someone wants them enough to add the dependency.

Steps 1 and 2 deliver most of what the user asked for.


User request relayed by @laoc_buzz in #edufeed-app. Code references are at dev @ 09842a48.

## The request On a resource card, when material is linked, a badge appears over the cover on hover: **"📎 1 Materialien verlinkt"**. A user asked whether the hover could say a little more about *what* is behind the card — e.g. "a linked PDF with 12 pages", "an image", "a PowerPoint with 24 slides" — rather than only how many things there are. ![linked-materials badge](https://git.edufeed.org/attachments/c7ba773d-2302-4d3e-9eb2-b58212e60132) (card: *"Anders sein" heißt einmalig sein…*, rpi-ekkw-ekhn.de). ## Where this lives today `src/lib/components/educational/AMBResourceCard.svelte:388-395` — the badge is **already** hover-only (`opacity-0 … group-hover:opacity-100`), so there is a place to put this; it just has nothing but a count in it. The count comes from `:74-78`: ```js const linkedMaterialsCount = $derived( (resource?.tags ?? []).filter((t) => t[0] === 'encoding:contentUrl' || t[0] === 'r').length ); ``` Note it merges two different things: **uploaded/attached files** (`encoding:contentUrl`) and **external references** (`r`). Those have very different metadata available, which shapes what is feasible below. ## What is already available — no new data needed `getAMBEncodings()` (`src/lib/helpers/educational/ambHelpers.js:330-351`) already returns per attached file: ```js { url, mimeType, size, sha256, name } ``` `mimeType` comes from `encoding:encodingFormat` and `size` from `encoding:contentSize`, both emitted at publish time (`src/lib/helpers/publication/publicationTags.js:127-131`). So **"a linked PDF (2.4 MB)", "an image", "a PowerPoint"** is achievable today from data already on the event. That is most of the user's request and it needs no format change, no migration, and no network call. Good first slice. ## What is *not* available — the page/slide counts AMB's `encoding:*` has no page or slide count, so nothing on the event carries it. Options, roughly in cost order: **PDFs are nearly free.** `src/lib/server/pdfThumbnail.js:27` already does: ```js const doc = await pdfjs.getDocument({ data, isEvalSupported: false }).promise; ``` `doc.numPages` is right there. The `/api/pdf-thumbnail` endpoint already fetches, renders and caches the file, and `pdfThumbnailGate.js` already implements the rights policy for when the app may touch it at all (open license, or an attested Blossom upload with a sha256). Returning a page count alongside the thumbnail — or from a sibling endpoint sharing the cache — reuses all of that. **This is the cheap path and I would do it first.** **Slide counts are not.** There is no PPTX handling anywhere in the app today. It would mean a new server-side dependency to unzip the OOXML and count `ppt/slides/slideN.xml`, plus the same rights gate. Real work, and worth deciding whether it earns its keep versus just saying "a PowerPoint". **Images** could carry dimensions the same way, though nothing extracts them today either. ## Three constraints worth knowing before scoping 1. **`r` tags carry no type at all** — just a URL. For external links the best available guess is the file extension, or an HTTP `HEAD` for `Content-Type` (a network call per card, with the same rights and privacy questions as any outbound fetch). Deciding what an external link renders as is part of this issue, not an afterthought. 2. **Multi-file resources have a pairing problem.** `getAMBEncodings()` aligns `encoding:contentUrl`, `encoding:encodingFormat` and `encoding:contentSize` **by position across separate tag lists**. The publish path notes this explicitly (`publicationTags.js:122-124`): *"Single file by design — the positional getAMBEncodings pairing can't disambiguate mixed link/upload runs with heterogeneous optional fields."* So if one of several files is missing a format or size, the indices shift and a later file gets labelled with an earlier file's type. A per-item hover list for multi-file resources needs that fixed — or needs to degrade to a summary when the tag counts do not line up. 3. **A long-tail question:** what does the hover show for 5 linked items? A list will not fit in a badge. Probably "PDF · 12 pages, 2 images, 1 more" or similar — a design call, not just an engineering one. ## Adjacent, small, and visible in the screenshot The badge reads **"1 Materialien verlinkt"** — wrong plural. Same in English: `"{count} linked materials"` renders "1 linked materials". Neither `messages/de.json:2742` nor `messages/en.json:2742` has a plural form, and there are currently **zero** ICU plural forms anywhere in `messages/de.json`. Whoever touches this string should fix that at the same time; if this is the first plural in the project it is worth checking how the Paraglide setup wants them expressed. ## Suggested slicing 1. Type + size from data already on the event, uploads only. Fixes the plural while in there. 2. PDF page count via the existing `/api/pdf-thumbnail` machinery and its rights gate. 3. External `r` links — decide the policy, then implement. 4. PPTX slide counts, only if someone wants them enough to add the dependency. Steps 1 and 2 deliver most of what the user asked for. --- User request relayed by @laoc_buzz in #edufeed-app. Code references are at `dev` @ `09842a48`.
laoc closed this issue 2026-07-31 10:12:14 +00:00
Author
Owner

Slices 1 and 2 shipped in #75, merged to dev at 65a16f1b.

  • Slice 1 — badge names type and size off the event (encoding:*), plural fixed: 📎 PDF · 2,4 MB.
  • Slice 2 — PDF page count: 📎 PDF · 12 Seiten · 2,4 MB. The thumbnail render writes numPages to a sidecar and /api/pdf-info reads it, so a card whose cover already rendered costs no second fetch. Gated on canDeriveThumbnail, attached files only (an r link carries no attestation).

Browser-verified by @TestOER at 88776df9 against an instrumented file host: the one-fetch claim with a fresh cache dir and a fresh URL per trial, plus the no-cover-first control that proves the counter can see a fetch at all, plus the rights gate in both the refusing and the must-allow direction.

Slices 3 and 4 are still open and need a product call (asked in the Buzz thread, unanswered):

  • 3, external r links — extension guess vs an HTTP HEAD per card to a third party on every feed render. My recommendation: extension guess, no network call.
  • 4, PPTX slide counts — needs a new server dependency to unzip OOXML. My recommendation: skip.

Related: #76. The sidecar handoff is correct but currently rarely taken, because thumbnail rendering fails for every PDF with non-embedded standard fonts. Pre-existing, and it does not change the behaviour of this feature — only its cost.

Slices 1 and 2 shipped in #75, merged to `dev` at `65a16f1b`. - **Slice 1** — badge names type and size off the event (`encoding:*`), plural fixed: `📎 PDF · 2,4 MB`. - **Slice 2** — PDF page count: `📎 PDF · 12 Seiten · 2,4 MB`. The thumbnail render writes `numPages` to a sidecar and `/api/pdf-info` reads it, so a card whose cover already rendered costs no second fetch. Gated on `canDeriveThumbnail`, attached files only (an `r` link carries no attestation). Browser-verified by @TestOER at `88776df9` against an instrumented file host: the one-fetch claim with a fresh cache dir and a fresh URL per trial, plus the no-cover-first control that proves the counter can see a fetch at all, plus the rights gate in both the refusing and the must-allow direction. **Slices 3 and 4 are still open and need a product call** (asked in the Buzz thread, unanswered): - **3, external `r` links** — extension guess vs an HTTP `HEAD` per card to a third party on every feed render. My recommendation: extension guess, no network call. - **4, PPTX slide counts** — needs a new server dependency to unzip OOXML. My recommendation: skip. Related: #76. The sidecar handoff is correct but currently rarely taken, because thumbnail rendering fails for every PDF with non-embedded standard fonts. Pre-existing, and it does not change the behaviour of this feature — only its cost.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app#57
No description provided.