feat(frontend): add thumbnailUrl helper with cache-bust param
Pure function returning /api/documents/{id}/thumbnail?v=<timestamp>
or null when thumbnailKey is missing. The encoded timestamp changes
whenever the backend regenerates a thumbnail (file replace),
invalidating browser caches despite the immutable Cache-Control.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
37
frontend/src/lib/thumbnails.test.ts
Normal file
37
frontend/src/lib/thumbnails.test.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { thumbnailUrl } from './thumbnails';
|
||||||
|
|
||||||
|
describe('thumbnailUrl', () => {
|
||||||
|
it('returns null when thumbnailKey is undefined', () => {
|
||||||
|
expect(thumbnailUrl({ id: 'abc' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns url without version param when thumbnailKey present but generatedAt missing', () => {
|
||||||
|
expect(thumbnailUrl({ id: 'abc', thumbnailKey: 'thumbnails/abc.jpg' })).toBe(
|
||||||
|
'/api/documents/abc/thumbnail'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends encoded cache-bust param when generatedAt present', () => {
|
||||||
|
const url = thumbnailUrl({
|
||||||
|
id: 'abc',
|
||||||
|
thumbnailKey: 'thumbnails/abc.jpg',
|
||||||
|
thumbnailGeneratedAt: '2026-04-22T20:41:15.123456'
|
||||||
|
});
|
||||||
|
expect(url).toBe('/api/documents/abc/thumbnail?v=2026-04-22T20%3A41%3A15.123456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('different generatedAt produces different URL — enables cache-bust on file replace', () => {
|
||||||
|
const a = thumbnailUrl({
|
||||||
|
id: 'x',
|
||||||
|
thumbnailKey: 'thumbnails/x.jpg',
|
||||||
|
thumbnailGeneratedAt: '2026-01-01T10:00:00'
|
||||||
|
});
|
||||||
|
const b = thumbnailUrl({
|
||||||
|
id: 'x',
|
||||||
|
thumbnailKey: 'thumbnails/x.jpg',
|
||||||
|
thumbnailGeneratedAt: '2026-01-01T11:00:00'
|
||||||
|
});
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
18
frontend/src/lib/thumbnails.ts
Normal file
18
frontend/src/lib/thumbnails.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
type ThumbnailDoc = {
|
||||||
|
id: string;
|
||||||
|
thumbnailKey?: string;
|
||||||
|
thumbnailGeneratedAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the URL for a document thumbnail image, or returns null when the document
|
||||||
|
* has no thumbnail yet. When `thumbnailGeneratedAt` is present it is appended as a
|
||||||
|
* `?v=…` query param so the browser / proxy cache is invalidated whenever the file
|
||||||
|
* is replaced (the backend regenerates thumbnails at the same S3 key on replace).
|
||||||
|
*/
|
||||||
|
export function thumbnailUrl(doc: ThumbnailDoc): string | null {
|
||||||
|
if (!doc.thumbnailKey) return null;
|
||||||
|
const base = `/api/documents/${doc.id}/thumbnail`;
|
||||||
|
if (!doc.thumbnailGeneratedAt) return base;
|
||||||
|
return `${base}?v=${encodeURIComponent(doc.thumbnailGeneratedAt)}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user