implement content-hash based image cache invalidation

Add content-based hashing to recipe images for proper cache invalidation
while maintaining graceful degradation through dual file storage.

Changes:
- Add imageHash utility with SHA-256 content hashing (8-char)
- Update Recipe model to store hashed filenames in images[0].mediapath
- Modify image upload endpoint to save both hashed and unhashed versions
- Update frontend components to use images[0].mediapath with fallback
- Add migration endpoint to hash existing images (production-only)
- Update image delete/rename endpoints to handle both file versions

Images are now stored as:
  - recipe.a1b2c3d4.webp (hashed, cached forever)
  - recipe.webp (unhashed, graceful degradation fallback)

Database stores hashed filename for cache busting, while unhashed
version remains on disk for backward compatibility and manual uploads.
This commit is contained in:
2026-01-02 12:06:53 +01:00
parent 6bf3518db7
commit ccf3fd7ea2
12 changed files with 603 additions and 38 deletions

View File

@@ -25,9 +25,12 @@ onMount(() => {
isloaded = document.querySelector("img")?.complete ? true : false
});
// Use germanShortName for images if available (English recipes), otherwise use short_name (German recipes)
const imageShortName = $derived(recipe.germanShortName || recipe.short_name);
const img_name = $derived(imageShortName + ".webp?v=" + recipe.dateModified);
// Use mediapath from images array (includes hash for cache busting)
// Fallback to short_name.webp for backward compatibility
const img_name = $derived(
recipe.images?.[0]?.mediapath ||
`${recipe.germanShortName || recipe.short_name}.webp`
);
</script>
<style>
.card_anchor{
@@ -261,7 +264,7 @@ const img_name = $derived(imageShortName + ".webp?v=" + recipe.dateModified);
<noscript>
<img class="image backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{recipe.alt}"/>
</noscript>
<img class="image backdrop_blur" class:blur={!isloaded} src={'https://bocken.org/static/rezepte/thumb/' + imageShortName + '.webp'} loading={loading_strat} alt="{recipe.alt}" on:load={() => isloaded=true}/>
<img class="image backdrop_blur" class:blur={!isloaded} src={'https://bocken.org/static/rezepte/thumb/' + img_name} loading={loading_strat} alt="{recipe.alt}" on:load={() => isloaded=true}/>
</div>
</div>
{#if showFavoriteIndicator && isFavorite}