This guide details the integration of Cloudflare R2-hosted images into your Hugo site, covering two distinct scenarios: embedding images directly within your Markdown content, and using them for theme-specific elements like featured images and backgrounds in the Blowfish theme.
Embedding R2 Images in Hugo Markdown Content #
This method allows you to use standard Markdown syntax for images, which are then automatically sourced from your Cloudflare R2 bucket.
1. Configure Hugo for R2 Access #
First, define your R2 public/custom domain and a special local prefix in your Hugo configuration file (hugo.toml
or config.yaml
). This prefix will signal which images in your Markdown should be sourced from R2.
hugo.toml
example:
[params]
# Your other params...
cdnBaseURL = "https://img.heykyo.com" # Replace with your R2 public/custom domain
cdnLocalAssetPrefix = "/cdn-assets/" # The prefix used in Markdown for R2 images
For Blowfish Theme (using params.toml
):
# Add the following in params.toml...
cdnBaseURL = "https://img.heykyo.com" # Replace with your R2 public/custom domain
cdnLocalAssetPrefix = "/cdn-assets/" # The prefix used in Markdown for R2 images
config.yaml
example:
params:
# Your other params...
cdnBaseURL: "https://img.heykyo.com" # Replace with your R2 public/custom domain
cdnLocalAssetPrefix: "/cdn-assets/" # The prefix used in Markdown for R2 images
2. Override the Image Render Template #
To enable this R2 integration, you need to override Hugo’s default Markdown image rendering. Create a file in your Hugo project at layouts/_default/_markup/render-image.html
. If your theme (like Blowfish) already has this file, copy it from the theme’s layouts/_default/_markup/render-image.html
to your project’s identical path to ensure you’re extending its functionality.
Paste the following code into your layouts/_default/_markup/render-image.html
:
{{- /* layouts/_default/_markup/render-image.html */ -}}
{{- $disableImageOptimization := .Page.Site.Params.disableImageOptimization | default false }}
{{- $url := urls.Parse .Destination }}
{{- $altText := .Text }}
{{- $caption := .Title }}
{{- /* ---- CDN Configuration ---- */ -}}
{{- $cdnBaseURL := site.Params.cdnBaseURL | default "" }}
{{- $cdnLocalAssetPrefix := site.Params.cdnLocalAssetPrefix | default "/cdn-assets/" }}
{{- /* ---- End CDN Configuration ---- */ -}}
{{- $isRemoteImage := findRE `^https?` $url.Scheme }}
{{- $isCdnPath := and (not $isRemoteImage) (hasPrefix $url.Path $cdnLocalAssetPrefix) }}
{{- if $isRemoteImage }}
{{- /* External image, use directly */ -}}
<figure>
<img class="my-0 rounded-md" loading="lazy" src="{{ $url.String }}" alt="{{ $altText }}" />
{{ with $caption }}<figcaption>{{ . | markdownify }}</figcaption>{{ end }}
</figure>
{{- else if and $cdnBaseURL $isCdnPath }}
{{- /* Local path starting with CDN prefix: rewrite to use cdnBaseURL */ -}}
{{- $cdnRelativePath := strings.TrimPrefix $cdnLocalAssetPrefix $url.Path }}
{{- /* Ensure robust slash handling between base URL and relative path */ -}}
{{- $finalCdnSrc := printf "%s/%s" (strings.TrimSuffix "/" $cdnBaseURL) (strings.TrimPrefix "/" $cdnRelativePath) }}
<figure>
<img class="my-0 rounded-md" loading="lazy" src="{{ $finalCdnSrc }}" alt="{{ $altText }}" />
{{ with $caption }}<figcaption>{{ . | markdownify }}</figcaption>{{ end }}
</figure>
{{- else }}
{{- /* Local path, not CDN prefix: use Blowfish theme's original logic for local resources */ -}}
{{- $resource := "" }}
{{- if $.Page.Resources.GetMatch ($url.String) }}
{{- $resource = $.Page.Resources.GetMatch ($url.String) }}
{{- else if resources.GetMatch ($url.String) }}
{{- $resource = resources.Get ($url.String) }}
{{- end }}
{{- with $resource }}
<figure>
{{- if or $disableImageOptimization (eq .MediaType.SubType "svg") }}
<img
class="my-0 rounded-md"
loading="lazy"
src="{{ .RelPermalink }}"
alt="{{ $altText }}"
/>
{{- else }}
<img
class="my-0 rounded-md"
loading="lazy"
srcset="
{{ (.Resize "330x").RelPermalink }} 330w,
{{ (.Resize "660x").RelPermalink }} 660w,
{{ (.Resize "1024x").RelPermalink }} 1024w,
{{ (.Resize "1320x").RelPermalink }} 2x"
data-zoom-src="{{ (.Resize "1320x").RelPermalink }}"
src="{{ (.Resize "660x").RelPermalink }}"
alt="{{ $altText }}"
/>
{{- end }}
{{ with $caption }}<figcaption>{{ . | markdownify }}</figcaption>{{ end }}
</figure>
{{- else }}
{{- /* Local path, but resource not found by Hugo. Output path as-is (relative to site). */ -}}
<figure>
<img class="my-0 rounded-md" loading="lazy" src="{{ $url.String | absURL }}" alt="{{ $altText }}" />
{{ with $caption }}<figcaption>{{ . | markdownify }}</figcaption>{{ end }}
</figure>
{{- end }}
{{- end }}
This code checks if an image path starts with your defined cdnLocalAssetPrefix
. If it does, it constructs the full URL using your cdnBaseURL
; otherwise, it falls back to the theme’s default handling for local images.
3. Usage in Markdown #
To insert an image from your R2 bucket into your Markdown content, use the standard Markdown image syntax 
, but start the image path with your cdnLocalAssetPrefix
. The rest of the path should exactly match the path to the image within your R2 bucket (relative to the cdnBaseURL
).
Example:
This is text in my post.

More text follows.
Assuming your cdnBaseURL
is https://img.heykyo.com
, this Markdown will be rendered in the final HTML as:
<img src="https://img.heykyo.com/photos/landscapes/sunset.jpg" alt="A picture from R2 bucket" ... />
Images in Markdown that do not start with /cdn-assets/
will be processed as regular local images by Hugo and your theme.
4. Important Note on Image Formats #
Make sure the images stored in R2 are in web-friendly formats that browsers widely support, such as JPEG, PNG, WebP, GIF, or SVG. Formats like HEIC have limited browser compatibility and may not display for all visitors. It’s best to convert HEIC images before uploading or use a service that can convert them on the fly if needed.
Using External R2 Images for Blowfish Theme Elements (Cards & Backgrounds) #
The Hugo Blowfish theme has excellent built-in support for featured images in article cards and hero sections. By default, it expects these images (e.g., featured.*
, background.*
) to be local resources within your post’s folder. This section explains how to modify Blowfish theme partials to prioritize images hosted on Cloudflare R2 for these elements, while retaining fallback logic for local images.
1. Define New Front Matter Parameter #
In your post’s Markdown front matter, introduce a new parameter, featuredImageExternal
, to specify an R2 URL for theme elements.
Example (YAML Front Matter):
---
title: "My Post with R2 Images"
date: 2023-10-28
featuredImageExternal: "https://your-r2-domain.com/path/to/card-or-featured-image.jpg"
tags: ["example"]
---
2. Modifying Article Card Images #
To use an external R2 image for the article summary cards displayed on list pages:
- File to Modify:
layouts/partials/article-link/card.html
- Action: Copy the original
themes/blowfish/layouts/partials/article-link/card.html
to your project’slayouts/partials/article-link/card.html
. Edit your project’s copy.
Modified layouts/partials/article-link/card.html
:
The key change is to check for .Params.featuredImageExternal
before the theme’s original image-finding logic.
{{ $disableImageOptimization := .Page.Site.Params.disableImageOptimization | default false }}
{{ $bgImageUrl := "" }} {{/* Initialize variable for the background image URL */}}
{{ $hideImage := .Params.hideFeatureImage | default false }} {{/* Check if image should be hidden */}}
{{ if not $hideImage }} {{/* --- Only proceed if image is not hidden --- */}}
{{/* 1. PRIORITIZE: Check for external featured image URL */}}
{{ with .Params.featuredImageExternal }}
{{ $bgImageUrl = . }} {{/* Use the external URL directly */}}
{{ else }}
{{/* 2. FALLBACK: If no external URL, use original Blowfish logic to find a LOCAL image */}}
{{ $images := $.Resources.ByType "image" }}
{{ $featured := $images.GetMatch "*feature*" }}
{{ if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end }}
{{/* Handling for '.Params.featureimage' - This seems complex, potentially involves remote fetch. */}}
{{/* We'll keep it but note that image optimization won't apply if it fetches a remote resource here */}}
{{ if and .Params.featureimage (not $featured) }}
{{ $url := .Params.featureimage }}
{{/* Attempt to fetch/get the resource. Handle potential errors if necessary. */}}
{{/* This might return a local or remote resource depending on the URL/setup */}}
{{ $featured = resources.GetRemote $url | default (resources.Get $url) }}
{{ end }}
{{/* Site default as the next fallback */}}
{{ if not $featured }}{{ with .Site.Params.defaultFeaturedImage }}{{ $featured = resources.Get . }}{{ end }}{{ end }}
{{/* Process the $featured resource if found (it should be a local resource or handled by GetRemote) */}}
{{ with $featured }}
{{/* Check if it's an SVG or optimization is disabled */}}
{{ if or $disableImageOptimization (strings.HasSuffix .RelPermalink ".svg") }}
{{ $bgImageUrl = .RelPermalink }} {{/* Use original relative permalink */}}
{{ else }}
{{/* Apply resize to the local image */}}
{{ with .Resize "600x" }}
{{ $bgImageUrl = .RelPermalink }} {{/* Use resized relative permalink */}}
{{ end }}
{{ end }}
{{ end }} {{/* End with $featured */}}
{{ end }} {{/* End else (no external URL) */}}
{{ end }} {{/* --- End if not $hideImage --- */}}
{{/* --- Link Structure --- */}}
{{ with .Params.externalUrl }}
<a href="{{ . }}" target="_blank" rel="external" class="min-w-full">
{{ else }}
<a href="{{ .RelPermalink }}" class="min-w-full">
{{ end }}
<div class="min-h-full border border-neutral-200 dark:border-neutral-700 border-2 rounded overflow-hidden shadow-2xl relative">
{{/* --- Display the Background Image using the determined $bgImageUrl --- */}}
{{ with $bgImageUrl }}
{{/* Use the determined URL (external or processed local) */}}
<div class="w-full thumbnail_card nozoom" style="background-image:url({{ . | absURL }});"></div>
{{ else }}
{{/* Optional: Add a placeholder or leave empty if no image found and not hidden */}}
{{/* Example: <div class="w-full thumbnail_card nozoom bg-gray-200 dark:bg-gray-700"></div> */}}
{{ end }}
{{/* --- Rest of the card content (Draft Label, Title, Meta, Summary) --- */}}
{{/* NOTE: Removed the original og:image meta tags from here as they are better handled in head partials */}}
{{ if and .Draft .Site.Params.article.showDraftLabel }}
<span class="absolute top-0 right-0 m-2">
{{ partial "badge.html" (i18n "article.draft" | emojify) }}
</span>
{{ end }}
<div class="px-6 py-4">
{{ with .Params.externalUrl }}
<div>
<div
class="font-bold text-xl text-neutral-800 decoration-primary-500 hover:underline hover:underline-offset-2 dark:text-neutral">
{{ $.Title | emojify }}
<span class="text-xs align-top cursor-default text-neutral-400 dark:text-neutral-500">
<span class="rtl:hidden">↗</span>
<span class="ltr:hidden">↖</span>
</span>
</div>
</div>
{{ else }}
<div class="font-bold text-xl text-neutral-800 decoration-primary-500 hover:underline hover:underline-offset-2 dark:text-neutral"
href="{{ .RelPermalink }}">{{ .Title | emojify }}</div>
{{ end }}
<div class="text-sm text-neutral-500 dark:text-neutral-400">
{{ partial "article-meta/basic.html" . }}
</div>
{{ if .Params.showSummary | default (.Site.Params.list.showSummary | default false) }}
<div class="py-1 prose dark:prose-invert">
{{ .Summary | plainify }}
</div>
{{ end }}
</div>
<div class="px-6 pt-4 pb-2">
{{/* Placeholder for potential footer content within the card */}}
</div>
</div> {{/* End main card div */}}
</a> {{/* End link */}}
3. Modifying Hero Background Images #
Blowfish offers several hero styles. We’ll focus on modifying the background.html
style as an example. You can apply a similar pattern to other hero styles.
- File to Modify:
layouts/partials/hero/background.html
- Action: Copy
themes/blowfish/layouts/partials/hero/background.html
to your project’slayouts/partials/hero/background.html
and edit your project’s copy.
Modified layouts/partials/hero/background.html
:
{{ $disableImageOptimization := .Page.Site.Params.disableImageOptimization | default false }}
{{ $bgImageUrl := "" }} {{/* Initialize variable for the final background image URL */}}
{{/* 1. PRIORITIZE: Check for external featured image URL in Front Matter */}}
{{ with .Params.featuredImageExternal }}
{{ $bgImageUrl = . }} {{/* Use the external URL directly */}}
{{ else }}
{{/* 2. FALLBACK: If no external URL, use original Blowfish logic to find an image resource */}}
{{ $featured := "" }} {{/* Initialize $featured within this scope */}}
{{ $images := .Resources.ByType "image" }}
{{ $featured = $images.GetMatch "*background*" }}
{{ if not $featured }}{{ $featured = $images.GetMatch "*feature*" }}{{ end }}
{{ if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end }}
{{/* Handle .Params.featureimage (potentially remote) */}}
{{ if and .Params.featureimage (not $featured) }}
{{ $url := .Params.featureimage }}
{{/* Use default "" if GetRemote fails, or if Get fails for local */}}
{{ $featured = resources.GetRemote $url | default (resources.Get $url | default "") }}
{{ end }}
{{/* Handle site default background */}}
{{ if not $featured }}
{{ with .Site.Params.defaultBackgroundImage }}
{{ if or (strings.HasPrefix . "http:") (strings.HasPrefix . "https:") }}
{{ $featured = resources.GetRemote . | default "" }} {{/* Use default "" if GetRemote fails */}}
{{ else }}
{{ $featured = resources.Get . | default "" }} {{/* Use default "" if Get fails */}}
{{ end }}
{{ end }}
{{ end }}
{{/* Process the $featured resource IF it was found (it should be a Resource object now) */}}
{{ with $featured }}
{{/* Check if it's an SVG or optimization is disabled - use original */}}
{{ if or $disableImageOptimization (strings.HasSuffix .RelPermalink ".svg") }}
{{ $bgImageUrl = .RelPermalink }}
{{ else }}
{{/* Apply resize to the local/fetched image resource */}}
{{ with .Resize (print ($.Site.Params.backgroundImageWidth | default "1200") "x") }}
{{ $bgImageUrl = .RelPermalink }} {{/* Use resized relative permalink */}}
{{ end }}
{{ end }}
{{ end }} {{/* End with $featured (resource processing) */}}
{{ end }} {{/* End else (no external URL) */}}
{{/* --- Now use the determined $bgImageUrl to render the hero background --- */}}
{{ $isParentList := eq (.Scratch.Get "scope") "list" }}
{{ $shouldBlur := $.Params.layoutBackgroundBlur | default (or
(and ($.Site.Params.article.layoutBackgroundBlur | default true) (not $isParentList))
(and ($.Site.Params.list.layoutBackgroundBlur | default true) ($isParentList))
) }}
{{ $shouldAddHeaderSpace := $.Params.layoutBackgroundHeaderSpace | default (or
(and ($.Site.Params.article.layoutBackgroundHeaderSpace | default true) (not $isParentList))
(and ($.Site.Params.list.layoutBackgroundHeaderSpace | default true) ($isParentList))
) }}
{{/* Only render the background div structure IF we found an image URL */}}
{{ with $bgImageUrl }}
{{ $finalUrl := . | absURL }} {{/* Store the absolute URL */}}
{{ if $shouldAddHeaderSpace | default true}}
<div id="hero" class="h-[150px] md:h-[200px]"></div>
{{ end }}
<div class="fixed inset-x-0 top-0 h-[800px] single_hero_background nozoom"
style="background-image:url({{ $finalUrl }});">
<div class="absolute inset-0 bg-gradient-to-t from-neutral dark:from-neutral-800 to-transparent mix-blend-normal"></div>
<div class="absolute inset-0 opacity-60 bg-gradient-to-t from-neutral dark:from-neutral-800 to-neutral-100 dark:to-neutral-800 mix-blend-normal"></div>
</div>
{{ if $shouldBlur | default false }}
<div id="background-blur" class="fixed opacity-0 inset-x-0 top-0 h-full single_hero_background nozoom backdrop-blur-2xl"></div>
<script>
window.addEventListener('scroll', function (e) {
var scroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
var background_blur = document.getElementById('background-blur');
background_blur.style.opacity = (scroll / 300)
});
</script>
{{ end }}
{{ end }} {{/* End with $bgImageUrl */}}
4. Considerations for Other Blowfish Hero Styles #
The Blowfish theme includes other hero styles in its layouts/partials/hero/
directory, such as:
basic.html
big.html
thumbAndBackground.html
If you use these hero styles and wish to enable external R2 image support for them, you would need to apply a similar modification pattern: copy the relevant partial to your project’s layout directory and update it to check for .Params.featuredImageExternal
before attempting to find local image resources.
Key Considerations for R2 Image Integration #
When integrating Cloudflare R2 images with your Hugo site, keep the following points in mind:
-
Hugo Image Processing: Hugo’s powerful image processing functions (like
.Resize
,.Fit
, WebP conversion) only work on local resources. Images referenced via your new external parameters will be used as-is. You’ll need to ensure images on R2 are already optimized and correctly sized for their intended display. -
Testing: Thoroughly test posts with and without the external image parameters to ensure both scenarios work correctly.
By implementing these R2 integration strategies, you can significantly enhance your Hugo site’s performance, manageability, and scalability, keeping your Git repository lean and leveraging the power of Cloudflare’s global network for image delivery.