feat: add product image download pipeline for PageSpeed 100%

Downloads Printify CDN images via ImageDownloadWorker, processes
through Media pipeline (WebP conversion, AVIF/WebP variant generation),
and links to ProductImage via new image_id FK.

- Add image_id to product_images table
- ImageDownloadWorker downloads and processes external images
- sync_product_images preserves image_id when URL unchanged
- PreviewData uses local images for responsive <picture> elements
- VariantCache enqueues pending downloads on startup
- mix simpleshop.download_images backfill task

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-01 00:26:19 +00:00
parent c818d0399c
commit 1b49b470f2
12 changed files with 381 additions and 33 deletions

View File

@@ -19,16 +19,24 @@
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<.breadcrumb
items={[
%{label: "Home", page: "home", href: "/"},
%{
label: @product.category,
page: "collection",
href:
"/collections/#{@product.category |> String.downcase() |> String.replace(" ", "-")}"
},
%{label: @product.name, current: true}
]}
items={
[
%{label: "Home", page: "home", href: "/"}
] ++
if @product.category do
[
%{
label: @product.category,
page: "collection",
href:
"/collections/#{@product.category |> String.downcase() |> String.replace(" ", "-")}"
}
]
else
[]
end ++
[%{label: @product.name, current: true}]
}
mode={@mode}
/>

View File

@@ -1404,7 +1404,8 @@ defmodule SimpleshopThemeWeb.ShopComponents do
src =
if image_id do
"/images/#{image_id}/variant"
# Trailing slash so build_srcset produces /images/{id}/variant/800.webp
"/images/#{image_id}/variant/"
else
image_url
end
@@ -4012,10 +4013,15 @@ defmodule SimpleshopThemeWeb.ShopComponents do
available = Optimizer.applicable_widths(assigns.source_width)
default_width = Enum.max(available)
# Database images end with / (e.g., /images/{id}/variant/)
# Mockups use - separator (e.g., /mockups/product-1)
separator = if String.ends_with?(assigns.src, "/"), do: "", else: "-"
assigns =
assigns
|> assign(:available_widths, available)
|> assign(:default_width, default_width)
|> assign(:separator, separator)
~H"""
<picture>
@@ -4030,7 +4036,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
sizes={@sizes}
/>
<img
src={"#{@src}-#{@default_width}.jpg"}
src={"#{@src}#{@separator}#{@default_width}.jpg"}
srcset={build_srcset(@src, @available_widths, "jpg")}
sizes={@sizes}
alt={@alt}
@@ -4046,9 +4052,13 @@ defmodule SimpleshopThemeWeb.ShopComponents do
end
defp build_srcset(base, widths, format) do
# Database images end with / (e.g., /images/{id}/variant/)
# Mockups use - separator (e.g., /mockups/product-1)
separator = if String.ends_with?(base, "/"), do: "", else: "-"
widths
|> Enum.sort()
|> Enum.map(&"#{base}-#{&1}.#{format} #{&1}w")
|> Enum.map(&"#{base}#{separator}#{&1}.#{format} #{&1}w")
|> Enum.join(", ")
end
end

View File

@@ -34,13 +34,11 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
|> Enum.reject(fn p -> p.id == product.id end)
|> Enum.take(4)
# Build gallery images (filter out nils)
# Build gallery images from local image_id or external URL
gallery_images =
[
product.image_url,
product.hover_image_url,
product.image_url,
product.hover_image_url
image_src(product[:image_id], product[:image_url]),
image_src(product[:hover_image_id], product[:hover_image_url])
]
|> Enum.reject(&is_nil/1)
@@ -70,6 +68,14 @@ defmodule SimpleshopThemeWeb.ShopLive.ProductShow do
List.first(products)
end
# Build image source URL - prefer local image_id, fall back to external URL
defp image_src(image_id, _url) when is_binary(image_id) do
"/images/#{image_id}/variant/1200.webp"
end
defp image_src(_, url) when is_binary(url), do: url
defp image_src(_, _), do: nil
@impl true
def render(assigns) do
~H"""