Updating Devtoberfest Selfie art β
The Devtoberfest Selfie photo booth is a Vue 3 island at hugo-apps/src/selfie/, mounted by the Hugo layout hugo/layouts/devtoberfest/selfie.html. This guide covers how to swap or add its three art families: backgrounds (scene art behind the person), stickers (draggable brand art), and frames (advocate cut-outs you compose into).
Where the assets live β
All art is served as static files from a single tree β nothing is imported as a JS module, so the URL path is the contract:
hugo/static/images/devtoberfest/selfie/
βββ backgrounds/ # 1080Γ1080 opaque PNG
βββ stickers/ # 512Γ512 transparent PNG
βββ frames/ # advocate cut-out PNGs
βββ thumbnails/ # frame-picker thumbnails (same filenames as frames/)The island resolves everything from a base path (imgBase, default /images/devtoberfest/selfie), so a file dropped in backgrounds/pumpkin-patch.png is served at /images/devtoberfest/selfie/backgrounds/pumpkin-patch.png.
| Family | Enumerated in | Asset directory | URL pattern | Format |
|---|---|---|---|---|
| Backgrounds | hugo-apps/src/selfie/backgrounds.ts (BACKGROUNDS array) | .../selfie/backgrounds/ | {base}/backgrounds/{file}.png | square opaque PNG, β₯1080Γ1080 recommended |
| Stickers | hugo/layouts/devtoberfest/selfie.html (data-stickers CSV) | .../selfie/stickers/ | {base}/stickers/{file}.png | transparent PNG, ~512Γ512 |
| Frames | hugo/layouts/devtoberfest/selfie.html (data-frames CSV) | .../selfie/frames/ + .../thumbnails/ | {base}/frames/{name}.png, {base}/thumbnails/{name}.png | PNG |
The two families are configured in different places. Backgrounds are declared in a TypeScript array in the island source; stickers and frames are declared as comma-separated
data-*attributes in the Hugo layout. Editing the wrong one is the most common mistake.
About image dimensions β
Exact pixel dimensions are not required β every image is resampled by the Konva compositor at render time. The conventions above are about aspect ratio and sharpness, not hard limits:
- Backgrounds are stretched to fill the stage box (
setBackgroundincompose.tsdraws them atwidth: stageW, height: stageHβ source aspect is not preserved). The export canvas is 1080Γ1080 square (STAGE_WIDTH/STAGE_HEIGHTinconstants.ts), so use a square (1:1) source to avoid distortion. Supply β₯1080Γ1080 so the image is downscaled (crisp) rather than upscaled (soft). A smaller or non-square PNG still works β it just looks softer or distorted. - Stickers and frames are placed as draggable/resizable nodes and scaled to fit, so their pixel size is irrelevant. What matters is a transparent background (stickers) and matching the intended frame shape (frames). Larger sources downscale cleanly.
Backgrounds β
The picker order and labels come from the BACKGROUNDS array in hugo-apps/src/selfie/backgrounds.ts:
export const BACKGROUNDS: BackgroundDef[] = [
{ id: 'pumpkin-patch', label: 'Pumpkin patch', file: 'pumpkin-patch' },
{ id: 'teched-stage', label: 'On stage', file: 'teched-stage' },
{ id: 'terminal', label: 'Terminal', file: 'terminal' },
{ id: 'autumn-gradient', label: 'Autumn', file: 'autumn-gradient' },
{ id: 'starfield', label: 'Starfield', file: 'starfield' },
]The URL is built as {imgBase}/backgrounds/{file}.png. A 'none' option is prepended automatically by the picker.
Replace an existing background β
- Export a 1080Γ1080 opaque PNG.
- Overwrite the matching file in
hugo/static/images/devtoberfest/selfie/backgrounds/β keep the same filename (e.g.starfield.png). - No code change needed. Rebuild + deploy (see Publishing changes).
Add a new background β
Add
my-scene.png(1080Γ1080, opaque) tohugo/static/images/devtoberfest/selfie/backgrounds/.Append an entry to
BACKGROUNDSinhugo-apps/src/selfie/backgrounds.ts.filemust match the filename without the.png:ts{ id: 'my-scene', label: 'My scene', file: 'my-scene' },Because this touches island TypeScript, the island must be recompiled β a full
npm run build:all+ deploy is required.
Optional: author backgrounds as SVG β
scripts/gen-backgrounds.mjs rasterizes hand-authored SVGs to PNG via sharp. To use it, add an entry to its SVGS object (keyed by filename) and run the script; it writes into the backgrounds/ directory. This is optional β a finished PNG works just as well.
Stickers β
Stickers are not listed in a TS array. They're declared as a CSV in the layout hugo/layouts/devtoberfest/selfie.html:
data-stickers="devtoberfest-badge,sap-developers-lockup,pumpkin,confetti,star,speech-bubble"At mount time hugo-apps/src/selfie/main.ts splits this list, and stickers.ts maps each name to { name, file: name }, rendered at {imgBase}/stickers/{file}.png. (The island also defines an emoji glyph set in stickers.ts β those are text, not image assets.)
Replace an existing sticker β
- Export a 512Γ512 transparent PNG.
- Overwrite the matching file in
.../selfie/stickers/, keeping the same filename (e.g.pumpkin.png). - No code change needed. Rebuild + deploy.
Add a new sticker β
Add
my-sticker.png(512Γ512, transparent) tohugo/static/images/devtoberfest/selfie/stickers/.Append the filename (no extension) to the
data-stickersCSV inhugo/layouts/devtoberfest/selfie.html:htmldata-stickers="devtoberfest-badge,sap-developers-lockup,pumpkin,confetti,star,speech-bubble,my-sticker"This touches only the Hugo layout + static images β no island recompile needed, but a build + deploy still ships the new image.
Optional: author stickers as SVG β
scripts/gen-stickers.mjs mirrors gen-backgrounds.mjs for stickers (SVG β 512Γ512 PNG). Add a key to its SVGS object and run it to regenerate.
Frames (advocate cut-outs) β
Frames are the advocate backdrops the person composes into. Like stickers, they're a CSV in the layout:
data-frames="Antonio,Antonio2,Background,...,Witalij"Each name needs two files with matching names:
hugo/static/images/devtoberfest/selfie/frames/<Name>.pngβ the full frame.hugo/static/images/devtoberfest/selfie/thumbnails/<Name>.pngβ the picker thumbnail.
To add/replace a frame: add both files, then add the name to data-frames in selfie.html. Frames are externally-sourced photos (no SVG generator).
Where to place new art β
Put finished PNGs directly under hugo/static/images/devtoberfest/selfie/<family>/. If you want the asset to be regenerable/versioned as vector source, add the SVG to the SVGS object in the matching scripts/gen-*.mjs and commit that too. Match the format conventions in the table above β wrong dimensions or a background sticker with an opaque canvas will look broken in the compositor.
Publishing changes β
Run a full build so Hugo picks up the static images and (for backgrounds) the island recompiles:
bashnpm run build:allIsland fingerprint gotcha β the global npmrc sets
ignore-scripts=true, sopostbuild:appslifecycle hooks don't fire locally.build:allcallsbuild:island-manifestexplicitly so hashed island bundles are referenced. Don't rely on a barevite build. See the "ignore-scripts silences postbuild:apps" note in the rootCLAUDE.md.Deploy. Static images ship inside the approuter as part of the normal MTA deploy β see MTA deployment. Confirm deploy scope (+content) with the maintainer.
Verify on the deployed site, not just locally: open the Devtoberfest selfie page and confirm the new/replaced art renders in the picker and composites correctly.
Quick reference β
| I want to⦠| Edit | Recompile island? |
|---|---|---|
| Swap a background image | overwrite backgrounds/<file>.png | No |
| Add a background | backgrounds/<file>.png + BACKGROUNDS in backgrounds.ts | Yes |
| Swap a sticker | overwrite stickers/<file>.png | No |
| Add a sticker | stickers/<file>.png + data-stickers in selfie.html | No |
| Add/swap a frame | frames/<Name>.png + thumbnails/<Name>.png + data-frames in selfie.html | No |
Related β
- Frontend apps β how
hugo-apps/islands are built and deployed - MTA deployment β deploy runbook
- Design specs:
docs/superpowers/specs/2026-08-08-selfie-generative-bg-design.md,...-selfie-stickers-captions-design.md,...-selfie-polaroid-frame-design.md