Production-ready Node.js external API that renders Discord leveling profile cards. Animated GIFs, with PNG fallback.
- Animated GIF output using
gifencoder(shimmer over progress bar) - PNG fallback if GIF path is not requested/allowed or fails
- Robust background loader with 7s timeout, Accept:
image/*, User-Agent, and first-frame decode for GIFs (incl. Giphy URLs) - Optional Bearer auth via
API_TOKEN - Input validation (hex colors, blur 0..20, URL checks)
- Simple TTL cache keyed by
guildId:userId:format
- Node.js >= 18.19.0 (LTS recommended; Node 20 is ideal)
- System libs for
node-canvas(server should already be set up if you’re running this)- Debian/Ubuntu (if you need to install):
sudo apt-get update && sudo apt-get install -y libcairo2 libpango-1.0-0 libjpeg-turbo8 libgif7 libpng16-16 fontconfig fonts-dejavu-core fonts-noto-color-emojisudo fc-cache -f -v
- Debian/Ubuntu (if you need to install):
PORTdefault3000API_TOKENoptional; if set, requireAuthorization: Bearer TOKENCACHE_SECONDSdefault300HTTP_TIMEOUT_MSdefault7000MAX_CANVAS_Wdefault900MAX_CANVAS_Hdefault300
npm installNote: canvas is required at runtime. If installation fails, ensure system libs are present (see Requirements).
PORT=3000 API_TOKEN=yourtoken node server.jsHealth check:
curl -s http://localhost:3000/health- GET
/health→{ "status": "ok" } - POST
/render/profile→ raw image bytes- Prefers GIF when requested and when
prefs.renderGIFsis true; otherwise PNG - Sets
Content-Type: image/giforimage/png - Caches by
guildId:userId:format
- Prefers GIF when requested and when
Headers you can send:
Accept: image/gif, image/png;q=0.8X-Render-Format: gif(explicitly prefer GIF)Authorization: Bearer TOKEN(only ifAPI_TOKENset)
Request JSON (example):
{
"guildId": "123",
"userId": "456",
"userTag": "User#0001",
"display": "Display Name",
"totals": { "level": 8, "prestige": 1, "xp": 1234, "chatXP": 900, "voiceXP": 334, "stars": 5 },
"progress": { "level": 8, "currLevelXP": 1000, "nextLevelXP": 1600, "intoLevel": 234, "toNext": 600, "progress": 0.39 },
"prefs": {
"showNick": true,
"style": "default",
"background": "https://picsum.photos/1200/800",
"nameColor": "#ffffff",
"statColor": "#cccccc",
"barColor": "#57f287",
"font": "default",
"blur": 4,
"renderGIFs": true
},
"settings": {
"embedsOnly": false,
"algorithm": { "base": 100, "exp": 2 }
}
}Responses:
- 200 +
Content-Type: image/giforimage/pngwith bytes - 400
{ "error": "..." }invalid payload - 401
{ "error": "unauthorized" }when token required/missing - 503
{ "error": "render_unavailable" }rendering unavailable - 500
{ "error": "internal_error" }
- Backgrounds may be PNG/JPEG or GIF (incl. Giphy CDN URLs with query params)
- Loader behavior in
src/renderer.js:- Always fetch with headers:
User-Agent: plex-levels-renderer/1.1,Accept: image/*, timeout 7s - Detect GIF by extension and/or magic bytes
- Try first-frame decode via
gifwrap; fall back tocanvas.loadImageif needed - On failure, log a concise record (status, content-type, bytes, final URL, magic) and render a neutral background
- Always fetch with headers:
curl -X POST http://localhost:3000/render/profile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Accept: image/gif, image/png;q=0.8" \
-H "X-Render-Format: gif" \
--data @- > out.gif <<'JSON'
{
"guildId": "1",
"userId": "2",
"userTag": "User#0001",
"display": "User",
"totals": { "level": 5, "prestige": 0, "xp": 850, "chatXP": 700, "voiceXP": 150, "stars": 3 },
"progress": { "level": 5, "currLevelXP": 700, "nextLevelXP": 1100, "intoLevel": 150, "toNext": 400, "progress": 0.375 },
"prefs": { "showNick": true, "style": "default", "background": "https://picsum.photos/1200/800", "nameColor": "#ffffff", "statColor": "#cccccc", "barColor": "#57f287", "font": "default", "blur": 2, "renderGIFs": true },
"settings": { "embedsOnly": false, "algorithm": { "base": 100, "exp": 2 } }
}
JSONRun the included quick test across PNG, static GIF, and Giphy animated GIF URLs:
TEST_URL=http://localhost:3000/render/profile npm run test:bg
# or against a remote service
TEST_URL=http://104.131.183.49:3000/render/profile API_TOKEN=yourtoken npm run test:bg- If
API_TOKENis set, all requests must sendAuthorization: Bearer TOKEN. - Remote background fetches honor a strict timeout; failures degrade gracefully.
- Fontconfig error: install fonts and rebuild cache
- Debian/Ubuntu:
sudo apt-get install -y fontconfig fonts-dejavu-core fonts-noto-color-emoji && sudo fc-cache -f -v
- Debian/Ubuntu:
- Canvas build issues: ensure
libcairo2,libpango-1.0-0,libjpeg-turbo8,libgif7,libpng16-16are installed beforenpm install. - Windows: prefer Node 20 LTS for prebuilt
node-canvasbinaries, or use WSL/Ubuntu.
