Changeset f79a471 in Klonkt for src/server.js


Ignore:
Timestamp:
06/28/2026 04:32:37 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
74c5abc
Parents:
1ff0043
git-author:
Robin Genis <roboburr@…> (06/28/2026 04:31:09 PM)
git-committer:
Robin Genis <roboburr@…> (06/28/2026 04:32:37 PM)
Message:

feat(images): on-demand lanczos cover thumbnails for crisp grid/list

High-res covers (esp. line-art) looked jagged because the browser downscaled them to
the grid/list size. Downscale server-side with ffmpeg lanczos to a small cached WebP
instead. No re-upload/backfill: reads the existing original lazily on first request.

  • services/ThumbnailService.js — ffmpeg lanczos downscale -> WebP, disk-cached
  • server.js — GET /media/thumb/:w/* route (whitelist 320/480/640) before the /media static
  • middleware/render.js — thumb(url,w) helper (rewrites local /media covers)
  • views/partials/post-tile.ejs (grid 480) + post-card.ejs (list 320)
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/server.js

    r1ff0043 rf79a471  
    2020import { SqliteSessionStore } from './services/SqliteSessionStore.js';
    2121import { ensurePrimarySite } from './services/ensurePrimarySite.js';
     22import { getThumbnail, THUMB_SIZES } from './services/ThumbnailService.js';
    2223import { resolveSite, loadAudioTracks, loadTheme } from './middleware/site.js';
    2324import { isViewer } from './middleware/auth.js';
     
    213214
    214215app.use('/assets', express.static(path.join(__dirname, 'assets'), { maxAge: isDev ? 0 : '1y' }));
     216
     217// On-demand cover thumbnails: /media/thumb/<w>/<path> → a small lanczos-downscaled WebP
     218// (cached on disk), so the browser doesn't jaggily shrink a high-res cover for the grid/
     219// list. Mounted BEFORE the /media static so it catches the thumb path first.
     220app.get('/media/thumb/:w/*', async (req, res) => {
     221  const w = parseInt(req.params.w, 10);
     222  const rel = req.params[0] || '';
     223  if (!THUMB_SIZES.has(w)) return res.status(400).end();
     224  let file = null;
     225  try { file = await getThumbnail(rel, w); } catch { /* fall through to original */ }
     226  if (!file) {
     227    // Generation unavailable/failed → serve the original instead of 404'ing.
     228    return res.redirect(302, '/media/' + rel.split('/').map(encodeURIComponent).join('/'));
     229  }
     230  res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
     231  res.setHeader('Cache-Control', isDev ? 'no-cache' : 'public, max-age=31536000, immutable');
     232  res.type('webp');
     233  res.sendFile(file);
     234});
     235
    215236app.use('/media', express.static(process.env.MEDIA_PATH || './storage/media', {
    216237  // Public media (post covers, avatars) must be cross-origin embeddable by other
Note: See TracChangeset for help on using the changeset viewer.