Course Imagery¶
Every course can carry two pieces of artwork, returned on GET /courses and
GET /courses/{id}:
| Field | What it is | Shape |
|---|---|---|
thumbnailUrl |
Card artwork, for catalog grids and lists | 3:2 landscape |
heroImageUrl |
Banner artwork, for a course detail page | 2:1 landscape |
Both are absolute URLs on FSRI's public image CDN. They need no signing, no API
key and no token, so you can put them straight into an <img> tag. A course
with no artwork omits the field rather than returning null.
Always request a size
The URLs point at full resolution masters. On the production catalog a
hero master is up to about 5 MB and a thumbnail up to about 1.6 MB.
Append ?width= and ?format=auto as described below and the same image
arrives in kilobytes. There is no reason to serve a master to a browser.
Resizing and format conversion¶
Add query parameters to the image URL itself. Nothing about the API call changes.
Measured on published courses, that turns a 662 KB thumbnail into a 4 KB
image, and a multi megabyte hero into roughly 66 KB at
?width=1920&format=auto.
Parameters go on the image URL, not on the API
Partner API endpoints reject unknown query parameters with a 400. Calling
GET /courses?width=400 is an error. Take thumbnailUrl from the response
and add the parameters to that.
| Parameter | Accepts | Behavior |
|---|---|---|
width |
any positive integer | Snapped to the nearest supported width (below). Height follows the aspect ratio. |
format |
auto, webp, jpeg, avif, png |
auto picks the best format the browser accepts. See below. |
quality |
any positive integer | Snapped to 60, 75 or 85. Default 75. |
All three are optional, and any combination works. Leave all three off and you get the original file, unchanged.
Supported widths¶
A width that is not on this list snaps to the nearest one instead of
failing. Ask for 333 and you receive the 320 wide image. The largest width
is 2560; anything larger snaps to it.
Images are never enlarged. If you ask for a width larger than the master,
you get the master's own width. Some thumbnail masters in the catalog are 648
pixels wide, so ?width=1280 on those returns a 648 pixel image. Lay out your
UI by aspect ratio, not by assuming the delivered pixel width.
format=auto¶
auto reads the browser's Accept header and returns WebP where the
browser supports it and JPEG otherwise. Every current browser gets WebP.
It deliberately never returns AVIF. On this catalog AVIF files measured 29 to
44 percent larger than WebP at every width, because the artwork is
illustration rather than photography. You can still request format=avif
explicitly.
Use format=png only if you need lossless output; it is much larger.
Examples¶
A responsive card image. The browser picks the smallest width that fills the slot, so phones download less than desktops.
<img
src="{thumbnailUrl}?width=400&format=auto"
srcset="{thumbnailUrl}?width=320&format=auto 320w,
{thumbnailUrl}?width=640&format=auto 640w,
{thumbnailUrl}?width=1024&format=auto 1024w"
sizes="(max-width: 600px) 100vw, 320px"
width="320" height="213"
alt="{course title}"
loading="lazy">
Set width and height in the 3:2 ratio (2:1 for heroes) so the page does
not shift while the image loads.
function imageUrl(baseUrl, width, { format = 'auto', quality } = {}) {
if (!baseUrl) return null; // course has no artwork
const url = new URL(baseUrl);
url.searchParams.set('width', String(width));
url.searchParams.set('format', format);
if (quality) url.searchParams.set('quality', String(quality));
return url.toString();
}
const card = imageUrl(course.thumbnailUrl, 400);
const banner = imageUrl(course.heroImageUrl, 1920);
from urllib.parse import urlencode
def image_url(base_url, width, fmt="auto", quality=None):
if not base_url: # course has no artwork
return None
params = {"width": width, "format": fmt}
if quality:
params["quality"] = quality
return f"{base_url}?{urlencode(params)}"
card = image_url(course.get("thumbnailUrl"), 400)
banner = image_url(course.get("heroImageUrl"), 1920)
function imageUrl(?string $baseUrl, int $width, string $format = 'auto', ?int $quality = null): ?string
{
if ($baseUrl === null) {
return null; // course has no artwork
}
$params = ['width' => $width, 'format' => $format];
if ($quality !== null) {
$params['quality'] = $quality;
}
return $baseUrl.'?'.http_build_query($params);
}
$card = imageUrl($course['thumbnailUrl'] ?? null, 400);
$banner = imageUrl($course['heroImageUrl'] ?? null, 1920);
What you can rely on¶
Aspect ratios. Every thumbnail in the catalog is 3:2 and every hero is 2:1. Pixel dimensions of the masters vary, so design to the ratio.
A URL never changes its picture, but it can stop existing. When FSRI
replaces a course's artwork, the new image gets a new URL and the old file
is removed, so the old URL starts returning 404. The course's updatedAt
moves at the same time, so a delta sync picks up the new URL.
In practice: take image URLs from your most recent sync rather than storing
them permanently, and if an image returns 404, re-fetch that course. Caching
the image bytes under their URL is safe, because a URL is never reused for a
different picture.
Same request, same bytes. A given URL and parameters always produce the same
image. Two requests that snap to the same variant, such as ?width=638 and
?width=640, return identical files.
Caching. Images are served with Cache-Control: public, max-age=86400, so
browsers and your own CDN may cache them for a day. Resized variants are also
cached at FSRI's edge, so the first request for a new size can take a moment
longer than later ones.
Cross origin use. Images are served with Access-Control-Allow-Origin: *.
You can fetch() them from your own pages or draw them to a canvas. For canvas
use, add crossorigin="anonymous" to the <img> tag, or the canvas is marked
as tainted and cannot be read back.
Missing images. A URL that does not exist returns 404. Unknown query
parameters are ignored.
About smallThumbnailUrl¶
The course schema also lists smallThumbnailUrl. It is absent from every
course and will stay that way: it predates URL resizing and no artwork was
ever produced for it. Do not build against it. For a small thumbnail, use
thumbnailUrl with a small width, for example ?width=160&format=auto.