Skip to content
Blur Placeholder
Esc
navigateopen⌘Jpreview
On this page

Delivery API

Request the placeholder through the Delivery API and render it in Next.js or Nuxt, including native BlurHash and ThumbHash values.

The package leaves Umbraco’s shallow media converter alone, so blurPlaceholder never appears in a media response you did not ask for. You request it explicitly instead.

Start by enabling the Delivery API and its media endpoints in the host application:

{
  "Umbraco": {
    "CMS": {
      "DeliveryApi": {
        "Enabled": true,
        "PublicAccess": true,
        "Media": {
          "Enabled": true,
          "PublicAccess": true
        }
      }
    }
  }
}

Then ask for the property by name:

GET /umbraco/delivery/api/v2/media/item/{mediaId}?expand=properties[$all]&fields=properties[blurPlaceholder]

The same expansion and field selection works for a media picker response. Every other media response keeps its usual shape, so the extra payload only reaches the clients that ask for it.

Example API response

The requested property appears in the media item’s properties object as one plain string:

{
  "path": "/station-platform.png/",
  "createDate": "2026-08-10T08:46:31.004237Z",
  "updateDate": "2026-08-10T08:46:31.004237Z",
  "id": "d55605e1-c63a-42cd-86a6-a99adca2a565",
  "name": "station-platform.png",
  "mediaType": "Image",
  "url": "/media/n1hbay0t/station-platform.png",
  "extension": "png",
  "width": 1536,
  "height": 1024,
  "bytes": 2468341,
  "properties": {
    "blurPlaceholder": "data:image/webp;base64,UklGRoIAAABXRUJQVlA4IHYAAABwAwCdASoQAAsALoVCoVClJSUlBQCESzgE6AxZblsod8ldbAAA/vs0YnnRmszUoA9/XeE6xi8oqvuYhNTIbmf34VbF388vudNZmZ7B4pF4N5Kwiixxuf2/w1XnA/yuEyJteMig85jSjuP1fcG9JRe+aOBYEAAA"
  },
  "focalPoint": {
    "left": 0.5,
    "top": 0.5
  },
  "crops": []
}

Render the default output

With the default DecodeToDataUrl: true, every algorithm produces a WebP data URL that can be passed directly to the framework’s image component.

import Image from "next/image";

export function MediaImage({ media }: { media: MediaItem }) {
  return (
    <Image
      src={media.url}
      alt={media.alt}
      width={media.width}
      height={media.height}
      placeholder={media.properties.blurPlaceholder ? "blur" : "empty"}
      blurDataURL={media.properties.blurPlaceholder ?? undefined}
    />
  );
}
<script setup lang="ts">
defineProps<{ media: MediaItem }>();
</script>

<template>
  <NuxtImg
    :src="media.url"
    :alt="media.alt"
    :width="media.width"
    :height="media.height"
    :placeholder="media.properties.blurPlaceholder || undefined"
  />
</template>

No browser decoder is involved, which is why this is the default. Remember the data URL is a placeholder; the real image still loads from media.url.

Consume a native hash

Set DecodeToDataUrl to false when the smaller stored value is worth decoding it on your application server.

Native values keep their prefix. blurhash: adds 9 characters and thumbhash: adds 10, and both compress to almost nothing once the Delivery API response uses gzip or Brotli. The prefix is what stops a configuration change from quietly feeding a ThumbHash to a BlurHash decoder.

Install blurhash, thumbhash, and sharp. Both examples below decode in a server component and pass only the resulting WebP data URL to the image component:

import Image from "next/image";
import { decodePlaceholder } from "@/lib/decode-placeholder.server";

// App Router components are Server Components unless marked "use client".
export async function MediaImage({ media }: { media: MediaItem }) {
  const placeholder = await decodePlaceholder(
    media.properties.blurPlaceholder,
    media.width,
    media.height,
  );

  return (
    <Image
      src={media.url}
      alt={media.alt}
      width={media.width}
      height={media.height}
      placeholder={placeholder ? "blur" : "empty"}
      blurDataURL={placeholder}
    />
  );
}
<script setup lang="ts">
import { decodePlaceholder } from "~/utils/decode-placeholder.server";

const props = defineProps<{ media: MediaItem }>();
const placeholder = await decodePlaceholder(
  props.media.properties.blurPlaceholder,
  props.media.width,
  props.media.height,
);
</script>

<template>
  <NuxtImg
    :src="media.url"
    :alt="media.alt"
    :width="media.width"
    :height="media.height"
    :placeholder="placeholder"
  />
</template>

Both components share one server-only decoder module. It branches on the prefix, so it handles either algorithm without extra configuration:

import sharp from "sharp";
import { decode } from "blurhash";
import { thumbHashToRGBA } from "thumbhash";

export async function decodePlaceholder(
  value: string | null | undefined,
  sourceWidth: number,
  sourceHeight: number,
): Promise<string | undefined> {
  if (!value) return undefined;
  if (value.startsWith("data:image/")) return value;

  if (value.startsWith("thumbhash:")) {
    return decodeThumbHash(value.slice("thumbhash:".length));
  }

  if (value.startsWith("blurhash:")) {
    return decodeBlurHash(value.slice("blurhash:".length));
  }

  throw new Error("Unknown blur placeholder representation.");

  async function decodeThumbHash(encodedHash: string) {
    const bytes = Uint8Array.from(Buffer.from(encodedHash, "base64"));
    const { w, h, rgba } = thumbHashToRGBA(bytes);
    return rgbaToWebpDataUrl(rgba, w, h);
  }

  function decodeBlurHash(hash: string) {
    const scale = 32 / Math.max(sourceWidth, sourceHeight);
    const width = Math.max(1, Math.round(sourceWidth * scale));
    const height = Math.max(1, Math.round(sourceHeight * scale));
    const rgba = decode(hash, width, height);
    return rgbaToWebpDataUrl(rgba, width, height);
  }
}

async function rgbaToWebpDataUrl(
  rgba: Uint8Array | Uint8ClampedArray,
  width: number,
  height: number,
) {
  const webp = await sharp(Buffer.from(rgba), {
    raw: { width, height, channels: 4 },
  })
    .webp({ quality: 60 })
    .toBuffer();

  return `data:image/webp;base64,${webp.toString("base64")}`;
}

Next.js App Router components are Server Components by default. Nuxt’s .server.vue component convention requires experimental.componentIslands: true in nuxt.config.ts. In both cases, the hash decoder and sharp stay on the server; the rendered component receives only the WebP data URL.

The utility calls the Wolt BlurHash JavaScript decoder and the official ThumbHash JavaScript implementation. BlurHash does not contain an aspect ratio, so the example derives its 32px decode size from the media dimensions; ThumbHash restores its encoded dimensions itself.

Was this page helpful?