Generates the social/OG preview images for solutions and profiles.
A request for e.g. /tracks/ruby/exercises/bob/solutions/ihid-1720000000.jpg
arrives via CloudFront at this Lambda's Function URL. The Lambda runs headless
Chrome against the corresponding page on exercism.org
(/images/solutions/ruby/bob/ihid), screenshots the #image-content element,
and returns it as a JPEG.
Generated images are written through to S3, so any given URL is only ever rendered once. This matters because rendering costs a few seconds of headless Chrome at 2GB, while serving a stored copy costs an S3 GET — around 300x less.
CDN edge caches alone don't give us that guarantee: they're per-PoP, they evict the long tail (most images are fetched only a handful of times ever), and a flood of requests for distinct URLs misses them entirely. Writing through to S3 makes cost a function of how many images exist rather than how many times they're requested.
URLs ending in -${timestamp}.jpg address a version that will never change, so
their stored copy is used indefinitely. Legacy URLs without a timestamp address
mutable content, so a stored copy is only reused for 24 hours — matching the
Cache-Control we hand back to the CDN.
If S3 is unreachable or the Lambda lacks permission, both reads and writes fail soft and the image is generated as normal.
| Variable | Default | Purpose |
|---|---|---|
IMAGE_BUCKET |
exercism-v3-assets |
Bucket holding generated images |
IMAGE_KEY_PREFIX |
generated-images |
Key prefix within that bucket |
NAVIGATION_TIMEOUT_MS |
6000 |
Page navigation timeout |
SELECTOR_TIMEOUT_MS |
6000 |
Timeout waiting for the content selector |
The two timeouts must stay comfortably below the Lambda's own timeout (20s). puppeteer defaults both to 30s, which is longer, meaning a hung render burned the full 20s at 2GB instead of failing fast.
The Lambda's execution role needs s3:GetObject and s3:PutObject on
arn:aws:s3:::${IMAGE_BUCKET}/${IMAGE_KEY_PREFIX}/*.