Phase 2b: images and downloads without ids, smaller originals, fewer files #4

Merged
pjotr merged 10 commits from feat/phase-2b-flat-media into main 2026-10-28 11:38:12 +00:00
Owner

Until now, an image or a download in the content was a 21-character id (mainImage: G69V5x3V2AHyKPi2dt3tA) that pointed to a record in content/media/, and that record held the file path. With the CMS gone, nobody can edit that by hand. After this PR the content holds the path and the alt text directly, and adding an image or a download takes one step. Every page shows the same text, the same images with the same alt text, and the same download links. Nothing changes on the server: same Dockerfile, compose file, pipeline and Traefik labels.

Commits:

  1. The flatten script (scripts/flatten-media.mjs), committed on its own so it can be read.
  2. The script's output, nothing edited by hand:
    • 23 image fields (15 atelier mainImage, 3 page banners, 5 on the home page) and 31 gallery images become { src, alt }. alt is the description the site already used as alt text. All 52 records had one, so no image lost its alt.
    • The 3 Kunstkuur videos become { url, description }, in the same order.
    • The 17 downloads buttons become Markdown links with the same label and the same href.
    • Each changed file parses to exactly the old data with the ids resolved, and the Markdown bodies are byte-identical.
  3. Components read the inline values. image, images, banner and videos no longer query content/media/. The downloads dispatch and elements/download.vue are gone. The gallery lightbox now gets the alt text too: it used to read a field that was never sent, so the enlarged image had none. The unused duotone layer in elements/image.vue is removed; no page turned it on.
  4. content/media/ deleted (52 image, 9 download and 3 video records).
  5. The script removed. It stays in the history only.
  6. 22 image files with spaces renamed, with every reference: the 12 aanbod card images (Aanbod banners-BAK.jpg becomes Aanbod-banners-BAK.jpg, aanbod banners-track.jpg becomes aanbod-banners-track.jpg) and the 10 frames of the animated home banner (animatie header_N.svg becomes animatie-header_N.svg). Case is kept. Downloads (PDFs) are never renamed: other sites and mails may link straight to them. None has a space today.
  7. 18 originals resized to at most 2400 px on the long side. Format and orientation are kept, JPEG at quality 82, and nothing is upscaled. The 18 files go from 52.5 MB to 18.3 MB; public/ goes from 131.7 MB to 97.4 MB. The git history still holds the old versions, so a fresh clone does not shrink. What shrinks is the working tree and the Docker build context.
  8. README: a new section, "Een afbeelding of download toevoegen". Put the file in public/assets/uploads/ and write the path and alt in the content; no id. The structure table and the aanbod field reference are updated, and the image fields of the fixed pages are documented.
  9. Download links look like the download buttons again (c2e1751). Two small content components, no Nuxt UI, so they stay in Phase 2c. components/content/prose/ProseA.vue renders a link with download as a plain <a> with the outlined button classes and the download icon; every other link is the normal link, as before. components/content/prose/ProseP.vue lets a paragraph that holds nothing but one download link flow inline, so short buttons sit side by side, as on the live site. The content stays plain Markdown links. The same commit fixes the file name: download now carries the file's own name instead of true, so a file is no longer saved as true.pdf (the live site has that bug). The README sentence on downloads says what the link becomes.
  10. 14 unused files removed (a677bef, 5.5 MB): the 9 *-duo.jpg and atelierkinderen.png (4.2 MB, read only by the duotone layer removed in commit 3) and public/assets/site/Textuur-1..3.svg and logo.svg (1.3 MB, referenced nowhere, on main either). Each was checked first: no hit for the raw name, the URL-encoded name or the bare stem in content, components, pages, layouts, CSS, config, deploy files or the text files in public/, and no request or link to any of them in a crawl of the built site (this branch and main).

No visible change for the download buttons. On /academie (6), /inschrijven (1) and 10 atelier pages, the download links are the same outlined buttons with the download icon as on the live site, from a plain Markdown link. The only difference a visitor notices: the file is saved under its own name instead of true.pdf.

Each download link ends with {target="_blank" download}. That suffix is needed: the download is what makes it a button, and without the suffix the site treats the PDF path as one of its own pages, and the click lands on "Deze pagina bestaat niet". This was tested, and the README says so.

Size: 139 files changed (54 of them images: 22 renamed, 18 resized, 14 removed); 318 lines added, 4505 removed (most of the removed lines are the three texture SVGs). public/ goes from 131.7 MB to about 91.9 MB.

Checked locally with Node 22.14.0 and pnpm 9.1.2 (Corepack), main (4b1caad) against the branch, both built and served side by side and crawled in headless Chromium at 1280 (the download pages also against the live site, at 375 and 1280):

  • A clean pnpm install --frozen-lockfile and pnpm build pass.
  • Routes: all 22 keep their status. The 20 pages give 200 (home, /aanbod, /academie, /inschrijven, /contact and 15 ateliers), and /nope and /aanbod/x/y give 404.
  • Images: the src and alt of all 92 <img> on those pages are identical. The only difference is which frame of the animated home banner was on screen when the shot was taken. The 15 card backgrounds on /aanbod are identical except the 12 renamed paths. All 10 banner frames return 200 under their new names.
  • Downloads: all 17 download links on 12 pages keep the same href, label and target="_blank"; download now holds the file name. All 9 PDFs return 200.
  • Download clicks: all 17 were clicked in headless Chromium. The page stays where it is (no 404), and the browser saves the file under its real name (for example lijst-vermindering.pdf; the live site and the branch before the fix give true.pdf).
  • Download buttons against the live site: /academie, /inschrijven and the Modeltekenen atelier, full page, at 375 and 1280 (and /academie at 768): pixel-identical to the live site, 0 pixels different, the download block included (same size, place, spacing and wrapping: Academie reglement and Lees ons taalbeleid side by side at 1280, stacked at 375). Before the fix the same blocks differed in 13 to 18 % of their pixels.
  • Focus: a download button with keyboard focus gets a 2 px black outline, 2 px outside it, 21:1 against the white page. Hover gives the same light green as live.
  • Assets: every /assets and /_ipx URL on the pages returns 200, except the 6 Banner aanbod 600x60px_*.svg. Those already 404 on main and are not part of this PR.
  • Text: the rendered text of all 22 routes has the same words in the same order.
  • Pixels at 1280, main against the branch: /aanbod is identical. / differs only in the animated banner frame (0.4 %). /academie, /inschrijven and the Modeltekenen atelier differed only where the buttons were text links before commit 9; with commit 9 they match live exactly (above). A gallery with 3 resized images (Schilderkunst) differs by 1.2/255 on average. Measured on the files themselves, the 18 resized images differ from their originals by 0.6/255 on average (worst 1.05/255) at 600 px wide.
  • Lightbox: opening a gallery image still shows the same file, and now with its alt text.

Not checked: Docker and Woodpecker (unchanged by this PR), widths other than 1280 outside the three download pages, Vimeo playback (the iframes get the same URLs in the same order).

Found, not changed:

  • The other buttons (the Link dispatches and the Inschrijven button) carry download="false", from the Nuxt UI button. No download happens today (the page links go through the router, the external one is cross-origin), but the attribute is wrong. That button is replaced in Phase 2c; drop the attribute there.
  • News items have an alt field that elements/news.vue never renders.
  • 21 images under 2400 px are still over 1 MB, mostly PNGs (largest liesbet.png, 3.9 MB). Converting them is a separate choice.

🤖 Generated with Claude Code

Until now, an image or a download in the content was a 21-character id (`mainImage: G69V5x3V2AHyKPi2dt3tA`) that pointed to a record in `content/media/`, and that record held the file path. With the CMS gone, nobody can edit that by hand. After this PR the content holds the path and the alt text directly, and adding an image or a download takes one step. Every page shows the same text, the same images with the same alt text, and the same download links. Nothing changes on the server: same Dockerfile, compose file, pipeline and Traefik labels. Commits: 1. **The flatten script** (`scripts/flatten-media.mjs`), committed on its own so it can be read. 2. **The script's output**, nothing edited by hand: - 23 image fields (15 atelier `mainImage`, 3 page banners, 5 on the home page) and 31 gallery images become `{ src, alt }`. `alt` is the description the site already used as alt text. All 52 records had one, so **no image lost its alt**. - The 3 Kunstkuur videos become `{ url, description }`, in the same order. - The 17 `downloads` buttons become Markdown links with the same label and the same href. - Each changed file parses to exactly the old data with the ids resolved, and the Markdown bodies are byte-identical. 3. **Components read the inline values.** `image`, `images`, `banner` and `videos` no longer query `content/media/`. The `downloads` dispatch and `elements/download.vue` are gone. The gallery lightbox now gets the alt text too: it used to read a field that was never sent, so the enlarged image had none. The unused duotone layer in `elements/image.vue` is removed; no page turned it on. 4. **`content/media/` deleted** (52 image, 9 download and 3 video records). 5. **The script removed.** It stays in the history only. 6. **22 image files with spaces renamed**, with every reference: the 12 aanbod card images (`Aanbod banners-BAK.jpg` becomes `Aanbod-banners-BAK.jpg`, `aanbod banners-track.jpg` becomes `aanbod-banners-track.jpg`) and the 10 frames of the animated home banner (`animatie header_N.svg` becomes `animatie-header_N.svg`). Case is kept. **Downloads (PDFs) are never renamed**: other sites and mails may link straight to them. None has a space today. 7. **18 originals resized** to at most 2400 px on the long side. Format and orientation are kept, JPEG at quality 82, and nothing is upscaled. The 18 files go from 52.5 MB to 18.3 MB; `public/` goes from 131.7 MB to 97.4 MB. The git history still holds the old versions, so a fresh clone does not shrink. What shrinks is the working tree and the Docker build context. 8. **README:** a new section, "Een afbeelding of download toevoegen". Put the file in `public/assets/uploads/` and write the path and alt in the content; no id. The structure table and the aanbod field reference are updated, and the image fields of the fixed pages are documented. 9. **Download links look like the download buttons again** (`c2e1751`). Two small content components, no Nuxt UI, so they stay in Phase 2c. `components/content/prose/ProseA.vue` renders a link with `download` as a plain `<a>` with the outlined button classes and the download icon; every other link is the normal link, as before. `components/content/prose/ProseP.vue` lets a paragraph that holds nothing but one download link flow inline, so short buttons sit side by side, as on the live site. The content stays plain Markdown links. The same commit fixes the file name: `download` now carries the file's own name instead of `true`, so a file is no longer saved as `true.pdf` (the live site has that bug). The README sentence on downloads says what the link becomes. 10. **14 unused files removed** (`a677bef`, 5.5 MB): the 9 `*-duo.jpg` and `atelierkinderen.png` (4.2 MB, read only by the duotone layer removed in commit 3) and `public/assets/site/Textuur-1..3.svg` and `logo.svg` (1.3 MB, referenced nowhere, on main either). Each was checked first: no hit for the raw name, the URL-encoded name or the bare stem in content, components, pages, layouts, CSS, config, deploy files or the text files in `public/`, and no request or link to any of them in a crawl of the built site (this branch and main). **No visible change for the download buttons.** On `/academie` (6), `/inschrijven` (1) and 10 atelier pages, the download links are the same outlined buttons with the download icon as on the live site, from a plain Markdown link. The only difference a visitor notices: the file is saved under its own name instead of `true.pdf`. Each download link ends with `{target="_blank" download}`. That suffix is needed: the `download` is what makes it a button, and without the suffix the site treats the PDF path as one of its own pages, and the click lands on "Deze pagina bestaat niet". This was tested, and the README says so. Size: 139 files changed (54 of them images: 22 renamed, 18 resized, 14 removed); 318 lines added, 4505 removed (most of the removed lines are the three texture SVGs). `public/` goes from 131.7 MB to about 91.9 MB. Checked locally with Node 22.14.0 and pnpm 9.1.2 (Corepack), main (`4b1caad`) against the branch, both built and served side by side and crawled in headless Chromium at 1280 (the download pages also against the live site, at 375 and 1280): - A clean `pnpm install --frozen-lockfile` and `pnpm build` pass. - **Routes:** all 22 keep their status. The 20 pages give 200 (home, `/aanbod`, `/academie`, `/inschrijven`, `/contact` and 15 ateliers), and `/nope` and `/aanbod/x/y` give 404. - **Images:** the `src` and alt of all 92 `<img>` on those pages are identical. The only difference is which frame of the animated home banner was on screen when the shot was taken. The 15 card backgrounds on `/aanbod` are identical except the 12 renamed paths. All 10 banner frames return 200 under their new names. - **Downloads:** all 17 download links on 12 pages keep the same href, label and `target="_blank"`; `download` now holds the file name. All 9 PDFs return 200. - **Download clicks:** all 17 were clicked in headless Chromium. The page stays where it is (no 404), and the browser saves the file under its real name (for example `lijst-vermindering.pdf`; the live site and the branch before the fix give `true.pdf`). - **Download buttons against the live site:** `/academie`, `/inschrijven` and the Modeltekenen atelier, full page, at 375 and 1280 (and `/academie` at 768): **pixel-identical to the live site**, 0 pixels different, the download block included (same size, place, spacing and wrapping: `Academie reglement` and `Lees ons taalbeleid` side by side at 1280, stacked at 375). Before the fix the same blocks differed in 13 to 18 % of their pixels. - **Focus:** a download button with keyboard focus gets a 2 px black outline, 2 px outside it, 21:1 against the white page. Hover gives the same light green as live. - **Assets:** every `/assets` and `/_ipx` URL on the pages returns 200, except the 6 `Banner aanbod 600x60px_*.svg`. Those already 404 on main and are not part of this PR. - **Text:** the rendered text of all 22 routes has the same words in the same order. - **Pixels at 1280, main against the branch:** `/aanbod` is identical. `/` differs only in the animated banner frame (0.4 %). `/academie`, `/inschrijven` and the Modeltekenen atelier differed only where the buttons were text links before commit 9; with commit 9 they match live exactly (above). A gallery with 3 resized images (Schilderkunst) differs by 1.2/255 on average. Measured on the files themselves, the 18 resized images differ from their originals by 0.6/255 on average (worst 1.05/255) at 600 px wide. - **Lightbox:** opening a gallery image still shows the same file, and now with its alt text. Not checked: Docker and Woodpecker (unchanged by this PR), widths other than 1280 outside the three download pages, Vimeo playback (the iframes get the same URLs in the same order). Found, not changed: - The other buttons (the `Link` dispatches and the `Inschrijven` button) carry `download="false"`, from the Nuxt UI button. No download happens today (the page links go through the router, the external one is cross-origin), but the attribute is wrong. That button is replaced in Phase 2c; drop the attribute there. - News items have an `alt` field that `elements/news.vue` never renders. - 21 images under 2400 px are still over 1 MB, mostly PNGs (largest `liesbet.png`, 3.9 MB). Converting them is a separate choice. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replaces every nanoid reference into content/media/ with inline values:
an image id becomes { src, alt } (from the entry's cover and description),
a video id becomes { url, description }, and a `downloads` dispatch becomes
a Markdown link with the same label and href. It edits lines in place, so
the rest of every file stays byte-identical, and it fails if any id is left.

It runs once in the next commit and is removed again afterwards, so it
only lives in the history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Output of `node scripts/flatten-media.mjs`, nothing edited by hand:
- 23 image fields (15 atelier mainImage, 3 page banners, 5 on the home
  page) and 31 gallery items become { src, alt }
- the 3 Kunstkuur videos become { url, description }
- 17 `downloads` dispatches become Markdown links with the same label
  and the same href
- every image entry had a description, so no alt is empty

Checked: each changed file parses to exactly the old data with every id
resolved, and the Markdown bodies are byte-identical. The components
still query by id until the next commit, so build this one together
with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The components no longer query content/media/ by id:
- elements/image, images and banner read { src, alt } from the content;
  the alt they render is the same text as before (the old description)
- the gallery lightbox now receives the alt too (it read a field the
  image never sent, so the enlarged image had no alt)
- elements/videos reads { url, description } in the given order
- the `downloads` dispatch and elements/download are gone: downloads are
  Markdown links now, with the same label, href, target and download

The unused duotone layer in elements/image goes with it: no page ever
turned it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 52 image, 9 download and 3 video entries are inline in the content
since the previous two commits. The files they pointed at stay in
public/; only the id records go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It ran once (see the history); keeping it would suggest the id model
still exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 aanbod card images in public/assets/uploads ("Aanbod banners-X.jpg"
becomes "Aanbod-banners-X.jpg") and the 10 frames of the animated home
banner ("animatie header_N.svg" becomes "animatie-header_N.svg").
Every reference moves with them: the `thumb` and `background` fields of
the aanbod and animatedBanner.vue. Case is kept, so no other name moves.

Not renamed on purpose:
- downloads (PDFs): external sites and mails may deep-link them. None
  has a space today anyway.
- public/assets/aanbod/Banner aanbod 600x60px_*.svg: category.vue asks
  for them in another case and from a relative path, so they 404
  already; that fix is a separate change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every raster image in public/ wider or taller than 2400 px now fits
2400 px on its long side; aspect ratio, orientation and format are kept
(JPEG at quality 82 with sips; the one PNG through Pillow with optimize,
because sips wrote it larger than the original). Nothing was upscaled;
SVG and GIF untouched; none of the 18 carried an EXIF orientation tag.

The 18 files go from 52.5 MB to 18.3 MB; public/ from 131.7 MB to
97.4 MB. IPX still resizes on request, so pages ask for the same URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New section "Een afbeelding of download toevoegen": put the file in
public/assets/uploads/, write the path and alt in the content. It also
covers file names, the 2400 px limit, the download link syntax, and why
an online PDF is never renamed. The structure table, the "what's where"
table and the aanbod field reference lose content/media/; the fixed-page
image fields are documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Markdown link with `download` ({target="_blank" download}) in any text field
renders as the outlined button with the download icon, as on the live site:
same size, padding, 14 px bold label, 4 px apart, side by side when they fit.
No ids, nothing special for the content editor.

- components/content/prose/ProseA.vue: a link with `download` is a plain <a>
  (never the router) with the button classes; any other link is the default
  NuxtLink. Tailwind only, no Nuxt UI, so it survives Phase 2c.
- components/content/prose/ProseP.vue: a paragraph holding nothing but one
  download link flows inline, as the old dispatch wrapper did.
- The file is saved under its own name: `download` now carries the file's
  basename instead of "true" (which saved every file as true.pdf, on live too).
- Focus: a 2 px black outline, 2 px out (21:1 on the white page).
- README: the download sentence says what the link turns into.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- the 9 *-duo.jpg and atelierkinderen.png (4.17 MB): only the duotone
  layer read them, and that layer went in 737d484
- public/assets/site/Textuur-1..3.svg and logo.svg (1.31 MB): not
  referenced anywhere, on main either

Checked before removing: no hit for the raw name, the URL-encoded name or
the bare stem in content, components, pages, layouts, CSS, config, deploy
or the text files in public/; the only hit in the build output is Nitro's
own list of public/ files; and no page of the crawled build (22 routes,
this branch and main) requests or links any of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pjotr merged commit 66920d6094 into main 2026-10-28 11:38:12 +00:00
Sign in to join this conversation.
No reviewers
No labels
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
sintlukas/sla-website!4
No description provided.