Stupid Problems Require Stupid Solutions (Cloudflare Is Breaking My SVGs)
Fixing problems that shouldn't exist
Cloudflare Pages is inexplicably mangling SVG attributes, causing icons to be malformed, missing colours, and broken. Attributes such as clip-path become clippath. After a day of trying to figure this out, creating reproductions, sanity checking against local builds, comparing with a Netlify-based deployment, and asking around on the Cloudflare Discord, I wrote a stupid solution to this stupid problem. I’m hoping it will only be temporary.lol
Background #
I’ve been improving how I handle icons on my site. The old process was frustrating and time-consuming: downloading SVGs, correcting misalignments, using SVG/XML linting tools, adding a viewBox and stripping out hardcoded heights and widths — seemingly, some icon authors do not believe in making icons easily resizable or even resizable. I often had to adjust alignments even with supposedly “consistent” icon sets like Font Awesome. Sadly, I doubt any of this is unfamiliar to anyone who has worked with icons and icon sets.
I found the unplugin-icons library which uses Iconify Design. I don’t intend to go into much detail as it’s not too relevant, but know it’s a vastly superior option for handling icons. You can see the icons over here.
Where did my colours go? #
Everything looked perfect locally, and it was a relief to finally be able to delete the collection of SVGs I’d been amassing. But the moment I deployed to Cloudflare Pages, I immediately ran into something incredibly bizarre. The icons were broken!
The simpler icons, like this RSS/Feed icon, were fine.

But not all of them were fine, mostly the ones with colours, gradients, and more complex shapes. Depending on the icon, this could mean some colours were simply different shades, or it could mean an entirely blacked-out icon.

Narrowing in on the cause #
The cause of this is broken SVG. Several attributes are consistently converted from valid to invalid. I still haven’t determined why this is Cloudflare Pages specific.
Every attribute with a hyphen is transformed into camel case. So clip-path becomes clipPath. This stood out to me because React (or, rather, JSX) uses camel cases for SVG attributes, which then get converted to their hyphenated form at build time - in other words, the opposite way around. This is not relevant, but it is an interesting observation nonetheless.

Excluding possibilities #
To ensure this really was a Cloudflare Pages-only problem, I ran a diff on the output of local builds, Netlify-based builds, and Cloudflare Pages-based builds to confirm.
This left a few possibilities, though I consider them unlikely. Firstly, I could exclude some possibilities based on the diffs:
- Astro, the static site generator I use. Based on the diffs this could be excluded.
- Node versions, already an improbable cause but I ensured that the same Node version was used everywhere anyway.
- The icon library mentioned previously. Again, based on the diffs this could also be excluded.
This then left several remaining possibilities:
- A setting in the Cloudflare dashboard and some build pipeline configuration. After eliminating as many variables as possible, I turned off various performance and miscellaneous settings. This had no impact and besides, why would there be any setting that causes this?
- A dependency of the icon library. It has some SVG dependencies, so I’ve not totally ruled this out, but even for the Node/NPM/JS ecosystem, I think it would be unlikely these dependencies would have a reason to output invalid SVG.
- Something really obvious I’ve missed. However, I was pretty confident, and I still am that I’d checked if there was anything I could be doing wrong, and after all the diffs made it was plainly obvious this only happens with Cloudflare.
- Cloudflare Pages build is doing some unwanted post-processing of the output.
- Cloudflare itself, for some reason.
Isolating the cause #
With the two strongest theories relating to Cloudflare, I had to narrow it down. When I commit, Cloudflare Pages is notified, pulls the updated branch, and builds it. Assuming this is successful, it’s deployed.
There is another way of deploying directly, though, without needing the Cloudflare Pages build process: with Wrangler, the Cloudflare CLI.
Once I deployed with Wrangler, I opened the live page and… the icons were not broken!

A small triumph - I knew at least one way to solve the issue. This was certainly a “solution,” except I would now need to write a GitHub Action to run Wrangler and use up my GitHub build minutes. I already have a lot of monthly build minutes available on Cloudflare Pages that I would prefer to use first.
Deploying to Another Cloudflare Pages Application #
At this point, I had to rule out the possibility of some outlandish configuration in my current Cloudflare Pages setup causing the issue. Was that possible? What setting would even cause clip-path to become clipPath?
To do that, I set up a brand new Cloudflare Pages application, connected it to the same repository, and deployed it. The icons were still broken, exactly like in the original deployment.
This confirmed that the issue wasn’t due to a misconfiguration in my initial project. Furthermore, the fact that the Wrangler deployment didn’t have the issue indicated the problem originated from the Cloudflare Pages build process, not from how the content was being served.
If it ain’t broke… Oh wait, it is. #
Not particularly wanting to start using my GitHub Actions build minutes, and with seemingly nothing wrongly configured with Cloudflare Pages, I was left with really only one solution: fix the SVGs after the build has run.
pnpm run build; pnpm run cloudflare:svgThe easy approach would be to run a string replacement over the build output, but this is far too destructive and error-prone. It would also stop me from writing this post!
I decided to do this properly, parse the SVG in the built HTML pages, and rename the broken attributes. I created a list of attributes that are being incorrectly renamed, plus some others, just in case any icons I use in the future use them.
const attributesToFix: { incorrect: string; correct: string }[] = [ { incorrect: 'cliprule', correct: 'clip-rule' }, { incorrect: 'clippath', correct: 'clip-path' }, { incorrect: 'fillrule', correct: 'fill-rule' }, { incorrect: 'stopcolor', correct: 'stop-color' }, { incorrect: 'stopopacity', correct: 'stop-opacity' }, { incorrect: 'strokelinecap', correct: 'stroke-linecap' },32 collapsed lines
{ incorrect: 'strokelinejoin', correct: 'stroke-linejoin' }, { incorrect: 'strokewidth', correct: 'stroke-width' }, { incorrect: 'strokemiterlimit', correct: 'stroke-miterlimit' }, { incorrect: 'strokedasharray', correct: 'stroke-dasharray' }, { incorrect: 'strokedashoffset', correct: 'stroke-dashoffset' }, { incorrect: 'strokeopacity', correct: 'stroke-opacity' }, { incorrect: 'fontfamily', correct: 'font-family' }, { incorrect: 'fontsize', correct: 'font-size' }, { incorrect: 'fontweight', correct: 'font-weight' }, { incorrect: 'fontstyle', correct: 'font-style' }, { incorrect: 'textdecoration', correct: 'text-decoration' }, { incorrect: 'textanchor', correct: 'text-anchor' }, { incorrect: 'dominantbaseline', correct: 'dominant-baseline' }, { incorrect: 'baselineshift', correct: 'baseline-shift' }, { incorrect: 'preserveaspectratio', correct: 'preserveAspectRatio' }, { incorrect: 'patternunits', correct: 'patternUnits' }, { incorrect: 'gradientunits', correct: 'gradientUnits' }, { incorrect: 'spreadmethod', correct: 'spreadMethod' }, { incorrect: 'maskunits', correct: 'maskUnits' }, { incorrect: 'filterunits', correct: 'filterUnits' }, { incorrect: 'floodcolor', correct: 'flood-color' }, { incorrect: 'floodopacity', correct: 'flood-opacity' }, { incorrect: 'lightingcolor', correct: 'lighting-color' }, { incorrect: 'enablebackground', correct: 'enable-background' }, { incorrect: 'colorinterpolation', correct: 'color-interpolation' }, { incorrect: 'colorinterpolationfilters', correct: 'color-interpolation-filters' }, { incorrect: 'colorprofile', correct: 'color-profile' }, { incorrect: 'colorrendering', correct: 'color-rendering' }, { incorrect: 'shaperendering', correct: 'shape-rendering' }, { incorrect: 'textrendering', correct: 'text-rendering' }, { incorrect: 'imagerendering', correct: 'image-rendering' },];The core of the script is simply iterating the nodes, finding the wrongly named attributes and renaming them.
const fixSVGAttributes = (content: string): [fixedContent: string, fixed: number] => { const dom = new JSDOM(content); const { document } = dom.window;
let fixed = 0;
const svgElements = document.querySelectorAll('svg, svg *'); for (const element of svgElements) { for (const attribute of attributesToFix) { if (element.hasAttribute(attribute.incorrect)) { const value = element.getAttribute(attribute.incorrect); if (value !== null) { element.removeAttribute(attribute.incorrect); element.setAttribute(attribute.correct, value); fixed++; } } } }
return [dom.serialize(), fixed];};The next part involves writing the new HTML back to the file, if there were any changes.
const processFile = async (filePath: string): Promise<void> => { try { const content = await fs.readFile(filePath, 'utf-8'); const [fixedContent, fixed] = fixSVGAttributes(content);
if (fixed > 0) { await fs.writeFile(filePath, fixedContent, 'utf-8'); console.log(`Updated file: ${filePath} - ${fixed} fixed attributes`); } } catch (error) { console.error(`Error processing file ${filePath}:`, error); }};The rest of the script involves finding HTML files in the output directory and running them through the SVG fixing logic. This is what I now see in my build log:
23:42:04.732 Updated file: dist/design/icons/index.html - 511 fixed attributes23:42:04.846 Updated file: dist/inbox/index.html - 18 fixed attributes23:42:04.862 Updated file: dist/index.html - 18 fixed attributes23:42:04.876 Updated file: dist/notes/1/index.html - 18 fixed attributesA fix, for now? #
I wish I had found some strange setting in the Cloudflare Pages configuration or something peculiar about one of the SVG library dependencies, but I didn’t. As I said previously, I haven’t totally ruled out those possibilities, but again, this SVG attribute mangling is only happening with the build process in Cloudflare Pages.
So, until a better fix is found for this stupid problem, I’ll use my stupid fix. In the grand scheme of things, this is not a major problem. I still really like using Cloudflare!