Devlog: Laying The Groundwork For Dynamic Header Images
Making some changes to the Card theme I’m using for this blog. First think I’m considering is a banner image, similar to the one in Scripting News. And like Scripting News, I’m hoping for the image to change occasionally. I’d like the change to happen when the blog is being built, and in order to do this, I need a way to configure this value. I’m hoping to use Blogging Tools to do this, but to actually make use of these values, I’m hoping to use Hugo’s resource data methods.
The idea is to setup an assets project which will be hosted on Netlify. When I want to change anything in it, Blogging Tools will push new changes via Git, which will run a pipeline that will deploy the updates to Netlify. Then, next time I add a blog post, Hugo will pick up these data fields and use it to build the site, probably by setting an attribute that will itself be picked by CSS’s newly improved attr() function to set the banner background.
This will start off as simple data values, stored in JSON, which will include things like the current banner header. One of the reasons why I’m keeping the actual data separate is that I don’t want any issues with Blogging Tools from impacting my ability to publish blog posts, and I have more confidence in Netlify’s ability to keep a service up than I do, especially as a company that is getting paid to do so.
Being a “backend guy”, I’ll start on the Netlify and Blogging Tools side of things first. Actually, I’ll start on the asset repo first, just so I can verify that Micro.blog’s Hugo instance can pull data like this.
I’ve added the new Git repo and got it publishing to Netlify on commit to main. At the moment there’s a single JSON file, /site/data.json that has a test message. The goal is now to get that onto my test blog and included in the Hugo template on build. Naturally, because Hugo loves changing their public API so often, I need to see how this is done in Hugo 0.117 as it seems to be completely different in Hugo 0.141, the current version as of this writing.
So, checking out the Hugo source code and doing a find/grep search to find the relevant technique. Turn’s out the way to do this in Hugo 0.117.0 is to use the getJSON function. Here’s a link to the docs for anyone else who’s interested.
<div>
{{ $siteData := getJSON "https://assets.lmika.org/data/site.json" }}
<div>My data is {{$siteData.data}}</div>
</div>
Okay, that seams to work:
So, the next thing to do is to add a banner image. I’m thinking of a slightly washed out version of the image, with a subtle gradient to the body housing the cards. I had to adjust the header template a little to move the site-header to a wrapping div. This allowed me to add new CSS to allow the header to span the entire width of the page. It’s really fortunate that the Card theme styles the CSS classes, and not the HTML elements themselves. It also allowed me to add a new HTML element with a header-end class to act as the gradient blend from the backing image to the regular background colour:
<!-- Template: layouts/partials/header.html -->
<header>
<div class="site-header">
<!-- The original content of 'header' -->
</div>
<div class="header-end"></div>
</header>
With that I could restyle the header:
/** Header */
body {
/* The body had a 10px padding which I had to disable to allow
the header to blend all the way to the client sides. */
padding: 0;
}
div.page-content,
footer.site-footer {
/* Disabling the 10px padding on the body meant I had to move
the padding here.*/
padding: 10px;
}
header {
/* Using 'background-blend-mode' to mix the header image with the background
colour. We're using 'color-mix' to make the background color slighty
transparent to do this. */
background-color: color-mix(
in srgb,
var(--body-background-color),
rgba(255,255,255,0.75)
);
background-image: url(https://lmika.org/uploads/2025/c1745b14d3.jpg);
background-blend-mode: screen;
background-size: cover;
background-position: 50% 50%;
margin: 0;
}
div.header-end {
/* A small gradient from transparent to a fully opaque background colour
allows a smooth transition from the header to the body. */
height: 20px;
background: linear-gradient(180deg,
rgba(0, 0, 0, 0) 0%,
var(--body-background-color) 100%
)
}
Various links to MDN documentation covering all these changes:
- background-blend-mode to blend the header image with the background colour. This is to wash it out some so that the menu items are not illegible.
- color-mix to set the transparency on the background colour.
- linear-gradient to generate a linear gradient.
Oh, and I naturally forgot dark mode, so added a new style to darken the image in that mode:
@media (prefers-color-scheme: dark) {
header {
background-color: color-mix(
in srgb,
var(--body-background-color),
rgba(0,0,0,0.5)
);
background-blend-mode: darken;
}
}
Okay, the next thing is to make the image dynamic. I want to try and use CSS attr() to do this. Basically the idea is that when the template is built, the image URL is pulled from the data and sent to the CSS via a HTML attribute. Here’s the new header template
<!-- Template: layouts/partials/header.html -->
{{ $siteData := getJSON "https://assets.lmika.org/data/site.json" }}
<header data-background-url="{{ $siteData.header.imageUrl }}">
And the updated CSS:
header {
background-image: url(attr(data-background-url raw-string));
}
Ah, that didn’t work. Apparently there are some restrictions on attr which could potentially result in security issues. So, plan B is to simply set the background-image in a style a style attribute:
<!-- Template: layouts/partials/header.html -->
{{ $siteData := getJSON "https://assets.lmika.org/data/site.json" }}
<header style="background-image: url({{ $siteData.header.imageUrl }})">
Okay, that’s working. Now the final test: can I make it dynamic? I’ll change the URL to another image:
{"header":{"imageUrl":"https://lmika.org/uploads/2025/pxl-20251105-200617867.jpg"}}
And publish a new post to rebuild the template. Let’s have a look:
Awesome, that’s working. Now to apply it to the actual blog. I did make one last change: the header attribute is used for the header of posts too, so I had to add a class — site-header-wrapper — to only select the site header. But yeah, it looks good. I like it. For reference, here’s the final template changes I’ve made:
<!-- Template: layouts/partials/header.html -->
{{ $siteData := getJSON "https://assets.lmika.org/data/site.json" }}
<header class="site-header-wrapper" style="background-image: url({{ $siteData.header.imageUrl }})">
<div class="site-header">
<!-- The original content of 'header' -->
</div>
<div class="header-end"></div>
</header>
And the final CSS changes:
/** Header */
body {
padding: 0;
}
div.page-content,
footer.site-footer {
padding: 10px;
}
header.site-header-wrapper {
background-color: color-mix(
in srgb,
var(--body-background-color),
rgba(255,255,255,0.75)
);
background-blend-mode: screen;
background-size: cover;
background-position: 50% 50%;
margin: 0;
}
@media (prefers-color-scheme: dark) {
header.site-header-wrapper {
background-color: color-mix(
in srgb,
var(--body-background-color),
rgba(0,0,0,0.5)
);
background-blend-mode: darken;
}
}
div.header-end {
/* A small gradient from transparent to a fully opaque background colour
allows a smooth transition from the header to the body. */
height: 20px;
background: linear-gradient(180deg,
rgba(0, 0, 0, 0) 0%,
var(--body-background-color) 100%
)
}
I didn’t have time to integrate this with Blogging Tools, but the groundwork has now been laid for that now.