when using S3 or cloudflare R2 for uploads alongside a custom CDN URL, custom emoji uploads do not respect the CDN configuration and attempt to load directly from the raw bucket URL.
The issue
when an admin uploads a custom emoji, the uploader creates an upload record and saves the raw bucket URL to the database (e.g., //my-bucket.s3.amazonaws.com/... or //my-bucket.r2.cloudflarestorage.com/...) - this is standard Discourse behavior.
however, when app/models/emoji.rb generates the emoji cache for /site.json, it passes the upload.url directly to the emoji object:
e.url = emoji.upload&.url
because it skips the CDN helper, the frontend receives the raw bucket URL. so depending on how strict the bucket’s access policies are, this results in broken images or forces Discourse to route the emojis through the internal avatar_proxy.
Solution
i have opened a PR that wraps the URL assignments in Discourse.store.cdn_url(), which makes the custom emoji loader align with how standard post images and avatars are routed.
Interim fix
while waiting for the PR to be reviewed and merged, i created a lightweight theme component that swaps the raw bucket URL for the proper CDN URL right before the custom emoji renders in the DOM (works for both posts and chat).
if you are experiencing this bug on your site, you can install this component and configure your S3 strings in the theme admin settings to fix any broken custom emojis:
Note: if you have already uploaded custom emojis that are currently borked, running discourse remap "//my-raw-bucket-url.com" "https://my-cdn.com" in the container will fix the old ones in the database, while the theme component will fix any newly uploaded ones until the PR fix is merged into core.
yea maybe it’s a cloudflare R2 only thing then. they are breaking on my instance. it only seems to break for new ones uploaded too.
without the theme component fix, i have to run the remap every time i upload a new one. the PR might need a bit of work - i’m not super good with the emoji code.
Discourse uses a two CDN setup, one for assets and one proxying the app.
Standard emoji uses one CDN, custom emoji uses the other CDN, but both are protected by a CDN on a properly configured website with a working two CDN setup.