Community Article
Community articles are authored by SitePoint Premium contributors. Content is screened before publication, and SitePoint reserves the right to moderate or remove articles that violate our guidelines. Views expressed are those of the authors and do not necessarily reflect those of SitePoint.
Build a Vite Plugin to Catch Broken SVG Assets Before Production
SASaifullah AdenwallaPublished inPlugins·Canvas & SVG·
August 25, 2026
·Updated:August 25, 2026
The AI briefing for Developers
Stay up to date with AI tools, model releases, and developer workflows that matter.
Weekly. Free. One click to leave.
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
- Premium Results
- Publish articles on SitePoint
- Daily curated jobs
- Learning Paths
- Discounts to dev tools
7 Day Free Trial. Cancel Anytime.
SVG files have an unusual position in frontend projects.
They look like ordinary static assets, but they’re also structured documents. They can contain paths, styles, IDs, embedded images, masks, filters, scripts, dimensions, and metadata. That flexibility is one of SVG’s strengths, but it also means a seemingly harmless asset change can introduce problems that aren’t discovered until much later.
A designer replaces a logo, and suddenly dark mode stops working. An exported SVG has no view box, so responsive sizing behaves unexpectedly. Another export contains hundreds of kilobytes of unnecessary editor metadata. A third includes markup that your team’s asset policy doesn’t allow.
None of these failures necessarily breaks the JavaScript build.
Vite happily starts.
The application renders.
The pull request gets merged.
The problem reaches production.
Instead of relying on developers to manually inspect every SVG, we can teach the build system what a valid project asset looks like.
In this tutorial, we’ll create a small custom Vite plugin that:
scans SVG assets↓validates project rules↓reports useful errors↓watches files during development↓blocks invalid production buildsThe interesting part isn’t SVG itself. It’s how Vite’s plugin system lets us turn project-specific knowledge into developer tooling.
If you’re relatively new to the build tool, Site Point’s introduction to Vite provides useful background on how Vite approaches development and production builds.
The Problem with Validation After Deployment
Suppose a project contains these files:
src/└── assets/└── brand/├── logo.svg├── logo-inverse.svg└── mark.svgThe frontend imports them normally:
importlogoUrlfrom"./assets/brand/logo.svg";and renders the resulting asset:
<imgsrc={logoUrl}alt="Acme"/>Everything looks straightforward.
But imagine someone replaces logo.svg with this:
<svgxmlns="http://www.w3.org/2000/svg"width="1200"height="400"><pathd="..."/></svg>The file contains no viewBox.
That might not immediately be obvious during review.
Or perhaps the new export is 750KB rather than 18KB because a design application included unnecessary information.
Again, the build succeeds.
What we really want is something closer to a type checker for assets.
When the project’s expectations aren’t satisfied, the developer should see:
SVG validation failed:src/assets/brand/logo.svg→ missing viewBox attributebefore the change is shipped.
Vite plugins are a good fit because they’re already part of the development and build lifecycle.
Start With a Small Vite Project
The plugin isn’t framework-specific, so you can use React, Vue, Svelte, or vanilla JavaScript.
For simplicity, assume we already have a Vite application.
Our project structure will eventually look like this:
project/├── src/│ ├── assets/│ │ └── brand/│ │ ├── logo.svg│ │ └── mark.svg│ ││ └── main.js│├── plugins/│ └── svg-validator.js│├── vite.config.js└── package.jsonInstead of publishing a plugin package immediately, we’ll keep the plugin inside the application.
That’s useful when developing project-specific tooling because we can establish whether the abstraction is actually useful before turning it into another dependency.
plugins/svg-validator.jsand start with the smallest possible plugin:
exportfunctionsvgValidator(){return{name:"svg-validator",buildStart(){console.log("SVG validator started");}};}Then load it from vite.config.js:
import{ defineConfig }from"vite";import{ svgValidator }from"./plugins/svg-validator.js";exportdefaultdefineConfig({plugins:[svgValidator()]});npm run devand our plugin becomes part of the Vite lifecycle.
This tiny example doesn’t accomplish much yet, but it establishes an important architectural boundary:
Application↓Vite↓Our project-specific pluginWe can now build validation into the same tool developers already use.
SitePoint has explored Vite from both introductory and architectural perspectives, including a deeper look at Vite’s plugin system anderic build tool needs to understand rules that are specific to your project
Finding the SVG Assets
Let’s tell the plugin where our files live.
We’ll use Node’s built-in file-system APIs rather than pulling in another dependency.
import{readdir,readFile,stat}from"node:fs/promises";importpathfrom"node:path";Now create a recursive function:
asyncfunctionfindSvgFiles(directory){const entries =awaitreaddir(directory,{withFileTypes:true});const files =[];for(const entry of entries){const fullPath = path.join(directory,entry.name);if(entry.isDirectory()){files.push(...awaitfindSvgFiles(fullPath));continue;}if(entry.isFile()&&entry.name.toLowerCase().endsWith(".svg")){files.push(fullPath);}}return files;}For this tutorial, we’ll validate everything under:
src/assets/brand/The plugin can resolve that path when it starts:
const assetDirectory = path.resolve("src/assets/brand");const files =awaitfindSvgFiles(assetDirectory);At this point, we’ve already moved beyond manually listing assets.
product-logo.svgnext week and it automatically becomes part of the validation process.
That’s exactly what we want from project tooling.
Define What “Valid” Means
The plugin can’t determine whether a logo is visually attractive.
It shouldn’t try.
Instead, we’ll define deterministic requirements.
For this project, let’s say every SVG must:
contain an <svg> rootcontain a viewBoxstay under 100KBavoid <script>avoid <foreignObject>These aren’t universal SVG laws.
They’re project policy.
That’s a useful distinction.
Build tools are excellent at answering:
Does this file satisfy the contract we agreed on?
Is this design good?
Keep subjective decisions with designers and deterministic decisions with tooling.
asyncfunctionvalidateSvg(filePath){const source =awaitreadFile(filePath,"utf8");const errors =[];if(!/<svg[s>]/i.test(source)){errors.push("missing <svg> root element");}if(!/bviewBoxs*=/i.test(source)){errors.push("missing viewBox attribute");}if(/<script[s>]/i.test(source)){errors.push("<script> elements are not allowed");}if(/<foreignObject[s>]/i.test(source)){errors.push("<foreignObject> is not allowed");}return errors;}Notice what this code is not doing.
It’s not pretending that regular expressions are a complete XML parser or an SVG security sanitizer.
We’re checking for a narrow set of patterns in files controlled by our project.
If you’re handling arbitrary user-supplied SVG documents, you need a much more defensive sanitization strategy.
That’s a separate problem from the repository-level developer tool we’re building here.
The distinction is important because frontend teams sometimes accidentally use build-time validation techniques as though they were security boundaries.
They’re not.
Enforce a File-size Budget
const fileStats =awaitstat(filePath);constMAX_SVG_SIZE=100*1024;That’s 100KB.
if(fileStats.size>MAX_SVG_SIZE){errors.push(`file exceeds 100KB budget`);}Our complete function becomes:
asyncfunctionvalidateSvg(filePath){const[source,fileStats]=awaitPromise.all([readFile(filePath,"utf8"),stat(filePath)]);const errors =[];if(!/<svg[s>]/i.test(source)){errors.push("missing <svg> root element");}if(!/bviewBoxs*=/i.test(source)){errors.push("missing viewBox attribute");}if(/<script[s>]/i.test(source)){errors.push("<script> elements are not allowed");}if(/<foreignObject[s>]/i.test(source)){errors.push("<foreignObject> is not allowed");}if(fileStats.size>MAX_SVG_SIZE){errors.push("file exceeds 100KB budget");}return errors;}A size budget is particularly useful for graphical assets because large regressions can otherwise be difficult to notice in code review.
14KB684KBThe visual difference may be negligible.
The delivery cost isn’t.
Rather than expecting the reviewer to manually inspect file sizes, the project can make the requirement executable.
Why SVG
Logo and icon assets increasingly come from many different workflows.
One designer may export from Illustrator.
Another uses Figma.
A startup team might experiment with an AI logo generator during early visual exploration before a designer cleans up the selected concept.
From Vite’s perspective, none of those origins matter.
The build tool sees a file.
What matters is whether the file entering production satisfies the technical constraints of the application.
That gives us a useful boundary:
Creative workflow↓Source SVG↓Technical validation↓Web applicationThe plugin doesn’t judge how the artwork was produced.
It judges whether the resulting asset can safely participate in our frontend workflow.
That makes the approach useful beyond branding.
The same architecture could validate:
UI iconsillustrationsdiagram assetspartner logospayment iconsproduct graphicswithout tying the build process to a particular design tool.
Validate the Entire Directory
Now create a function that processes every SVG:
asyncfunctionvalidateDirectory(directory){const files =awaitfindSvgFiles(directory);const problems =[];for(const file of files){const errors =awaitvalidateSvg(file);if(errors.length>0){problems.push({file,errors});}}return problems;}[][{file:"src/assets/brand/logo.svg",errors:["missing viewBox attribute","file exceeds 100KB budget"]}]The structured result matters.
Avoid logging errors deep inside the validation function when possible.
Returning data gives the plugin control over how problems are presented.
The same validator could later be used by:
ViteCIa Node CLIunit testsan editor extensionwithout rewriting the underlying rules.
Turn Validation Errors Into Build Errors
Now connect the validator to buildStart.
exportfunctionsvgValidator(){const assetDirectory =path.resolve("src/assets/brand");return{name:"svg-validator",asyncbuildStart(){const problems =awaitvalidateDirectory(assetDirectory);if(problems.length===0){return;}const message =formatProblems(problems);this.error(message);}};}functionformatProblems(problems){const lines =["SVG validation failed:",""];for(const problem of problems){lines.push(problem.file);for(const error of problem.errors){lines.push(`→${error}`);}lines.push("");}return lines.join("n");}logo.svghas no viewBox.
Vite produces a useful failure:
SVG validation failed:src/assets/brand/logo.svg→ missing viewBox attributethis.error(message);We aren’t creating an informational report that somebody might read.
We’re telling the build system:
The input violates our requirements, so the build should not continue.
That’s the difference between auditing and enforcement.
Make Development Feedback Immediate
Catching invalid assets during vite build is useful.
Catching them immediately after the developer saves the file is better.
Vite’s development server exposes its file watcher through the plugin API, so we can attach our validation logic during development.
configureServer(server){}We’ll create a reusable helper first:
asyncfunctionvalidateOrThrow(directory){const problems =awaitvalidateDirectory(directory);if(problems.length>0){thrownewError(formatProblems(problems));}}Then register our directory with the watcher:
configureServer(server){server.watcher.add(assetDirectory);}configureServer(server){server.watcher.add(assetDirectory);server.watcher.on("change",asyncfile=>{if(!file.toLowerCase().endsWith(".svg")){return;}if(!file.startsWith(assetDirectory)){return;}try{const errors =awaitvalidateSvg(file);if(errors.length){console.error(formatProblems([{file,errors}]));}}catch(error){console.error(error);}});}Now the feedback loop becomes:
Developer edits SVG↓Saves file↓Vite watcher detects change↓Plugin validates asset↓Terminal reports problemThat’s much better than discovering it during a later production build.
Vite’s current plugin API explicitly exposes the development server, including access to its file-system watcher, through configureServer.
Don’t Turn the Plugin Into a Second Build System
Custom developer tooling has a common failure mode.
validate SVGsand six months later it handles:
SVG validationPNG conversionWebP generationimage resizingfavicon generationmanifest generationCSS generationcomponent generationuploadsdeploymentAt that point, the plugin isn’t a plugin anymore.
It’s an application hiding inside the build configuration.
Keep responsibilities narrow.
Our Vite plugin should answer:
Are these assets valid according to our project rules?
It doesn’t necessarily need to:
Transform every media format our company might ever use.
That work can live in separate scripts or purpose-built image pipelines.
This is particularly important for user-uploaded media, which has very different requirements from repository-controlled assets. SitePoint’s recent tutorial on building a fast image upload pipeline in the browser with JavaScript explores that side of image processing, including client-side validation and resizing.
Our Vite plugin solves a different problem:
Developer-controlled source assetsUntrusted files uploaded by usersKeeping those concerns separate makes both systems easier to reason about.
Make the Plugin Configurable
Hard-coded rules are acceptable while experimenting.
If the plugin proves useful, give it a small configuration API.
exportfunctionsvgValidator({directory ="src/assets/brand",maxSizeKB =100,requireViewBox =true}={}){}svgValidator({directory:"src/assets/icons",maxSizeKB:40});const maxBytes =maxSizeKB *1024;if(requireViewBox &&!/bviewBoxs*=/i.test(source)){errors.push("missing viewBox attribute");}Now the plugin can support different asset groups.
exportdefaultdefineConfig({plugins:[svgValidator({directory:"src/assets/brand",maxSizeKB:100}),svgValidator({directory:"src/assets/icons",maxSizeKB:30})]});The important point isn’t adding endless configuration.
It’s exposing the requirements that genuinely vary between projects.
A good developer tool should have strong defaults and a small surface area.
Give Developers Useful Errors
Invalid SVG.SVG validation failed:src/assets/brand/logo-inverse.svg→ missing viewBox attributesrc/assets/brand/mark.svg→ file exceeds 100KB budget→ <foreignObject> is not allowedBoth technically communicate failure.
Only one helps someone fix it quickly.
Developer experience isn’t just about fast hot-module replacement.
It’s also about reducing the amount of detective work necessary when something goes wrong.
As your plugin grows, include enough context to answer:
Which file failed?Which rule failed?What was expected?How can I fix it?For a file-size failure, we could make the message even better:
const sizeKB =Math.ceil(fileStats.size/1024);errors.push(`file is${sizeKB}KB; maximum is${maxSizeKB}KB`);→ file is 184KB; maximum is 100KBThat’s immediately actionable.
Extract the Validation Engine
As the implementation matures, separate Vite-specific code from asset-specific code.
plugins/├── svg-validator/│ ├── index.js│ ├── validate-svg.js│ ├── find-files.js│ └── format-errors.jsvalidate-svg.js should know nothing about Vite.
exportasyncfunctionvalidateSvg(filePath,options){}index.js handles the Vite integration:
exportfunctionsvgValidator(options){return{name:"svg-validator",asyncbuildStart(){},configureServer(server){}};}This separation gives us a useful architecture:
SVG rules↑│Validation engine↑│┌──┴─────────┐│ │Vite plugin Future CLIIf you later want developers to run:
npm run validate:svgwithout starting Vite, the validation logic is already reusable.
Node.js is particularly well suited to small project utilities like this. SitePoint has a broader tutorial on building command-line tools with Node.js if you decide to expose your validator as a standalone CLI.
Use the Same Rules in CI
Local validation improves feedback.
CI provides enforcement.
Suppose your production build already runs:
npm cinpm run buildBecause our validator hooks into Vite’s build lifecycle, an invalid SVG automatically causes:
npm run buildto fail.
No separate CI integration is required.
Pull request↓Install dependencies↓Run Vite build↓SVG plugin validates assets↓Valid?↙ ↘yes no↓ ↓build CI failsThis is where custom tooling becomes particularly valuable.
Remember to make sure exported logos have a viewBox.
depends on somebody remembering the rule forever.
A plugin doesn’t remember.
It executes.
Every time.
Asset Rules Should Evolve With the Application
The five checks we’ve implemented are deliberately modest.
Real applications may have additional requirements.
A design system might expect every icon to use:
fill="currentColor"rather than hard-coded colors.
A logo directory might require both:
logo.svglogo-inverse.svgA mobile application might require the compact mark to use a square viewBox.
A project may prohibit embedded raster data such as:
<imagehref="data:image/png;base64,..."/>You can turn any deterministic requirement into another validation function.
functioncheckEmbeddedRaster(source){if(/data:image/(?:png|jpeg|webp);base64/i.test(source)){return["embedded raster images are not allowed"];}return[];}const validators =[checkViewBox,checkScripts,checkForeignObject,checkEmbeddedRaster];for(const validator of validators){errors.push(...validator(source));}This is much easier to maintain than a single enormous validation function.
And it mirrors how developer tooling tends to evolve: small rules composed into a predictable pipeline.
Be Careful With Build-tool Version Assumptions
Vite itself continues to evolve.
SitePoint’s more recent coverage of the Vite 8 production toolchain and migration concerns demonstrates how underlying transformation and build infrastructure can change between major versions.
That’s another reason to keep custom plugins focused on documented plugin hooks rather than reaching deeply into undocumented Vite internals.
Our plugin uses a small surface:
buildStart()configureServer()server.watcherthis.error()The less internal knowledge your plugin requires, the easier it is to maintain alongside future build-tool updates.
Custom tooling should extend your build system.
It shouldn’t couple your application to implementation details the build system never promised to preserve.
Where Developer Tooling Pays Off
The SVG validator we’ve built is intentionally small.
It won’t win awards for algorithmic complexity.
That’s part of what makes it useful.
Some of the best internal developer tools solve problems that are individually tri
Designer exports SVG↓Developer adds file↓Reviewer hopefully notices issues↓Someone manually optimizes it↓Someone checks dark mode↓ProductionWith an explicit build contract:
Designer exports SVG↓Developer adds file↓Vite validates automatically↓Invalid assets fail immediately↓ProductionWe’ve removed several opportunities for inconsistency without creating another manual process.
That principle extends far beyond SVGs.
The same Vite plugin architecture could enforce project rules for:
localization filesJSON configurationMarkdown frontmatterroute metadataenvironment-specific modulesdesign tokenscomponent documentationDoes our team repeatedly review the same deterministic rule by hand?
If the answer is yes, there’s probably an opportunity for developer tooling.
From Convention to Executable Contract
Teams naturally create conventions.
All brand SVGs must have viewBox attributes.That’s useful documentation.
But documentation and enforcement solve different problems.
Here’s what you should do.
The project won’t accept something else.
For requirements that can be checked deterministically, the second approach is often much more reliable.
Our SVG rule began as a sentence:
Every production logo should have a viewBox.It ended as executable JavaScript:
if(!/bviewBoxs*=/i.test(source)){errors.push("missing viewBox attribute");}That’s a small transformation, but it’s a powerful one.
The knowledge is no longer trapped in a document, code-review checklist, or one experienced developer’s memory.
It’s part of the build.
Final Thoughts
Vite is usually discussed in terms of fast development servers, bundling, framework integrations, and production builds.
Its plugin system is equally useful for something less glamorous: teaching a project about itself.
Generic tooling can’t know that your icon files should stay below 30KB.
It can’t know that your brand assets require viewBox.
It can’t know which SVG features your team has decided not to allow.
But your JavaScript can.
"Remember to check this.""The build checks this."That’s the real value of project-specific developer tooling.
Not more configuration.
Not another dependency.
Less knowledge that humans have to remember manually.


