Automatically Re-Executing Scripts in ASP.NET Core MVC with Blazor Enhanced Navigation
Lee Timmins
1st Oct 2025
Comments
In my previous post on executing scripts in an ASP.NET Core MVC app with Blazor enhanced navigation, you had to use a <page-script> tag to define your custom scripts. But what if you don’t want to manually convert every script on your site?
Here’s a way to automatically handle this by using a MutationObserver and reloading <script> tags whenever they appear in the DOM.
The Script Loader
Add the following to your global JavaScript file (e.g., site.js), making sure it’s included on every page:
function loadScript(script) {
// Create the new script.
const newScript = document.createElement('script');
// Prevent the new script from getting stuck in an infinite loop. Since the mutation observer will monitor for it.
newScript.dataset.ignoreNew = true;
// Add the attributes.
Array.from(script.attributes).forEach(a => newScript.setAttribute(a.name, a.value));
// Set the inner HTML.
newScript.appendChild(document.createTextNode(script.textContent));
// Replace the old script with the new script tag.
script.parentNode.replaceChild(newScript, script);
}
// Create a MutationObserver.
const observer = new MutationObserver(async mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
// Check if the added node is a script tag.
if (node.tagName?.toLowerCase() === 'script' && node.dataset.ignoreNew !== 'true')
loadScript(node);
else if (node.nodeType === 1)
node.querySelectorAll('script').forEach(scriptNode => loadScript(scriptNode));
}
}
});
// Start observing changes in the document's body.
observer.observe(document.body, { childList: true, subtree: true });
Example: Standard Script
If you have a script included like this:
<script src="/js/script.js"></script>
And /js/script.js contains:
console.log('Script');
You’ll now see "Script" logged every time you navigate back to a page that references this script — not just on the initial load.
Example: Module Script
Things get trickier when using JavaScript modules:
<script type="module" src="/js/script.js"></script>
Modules are cached by the browser once loaded. This means that navigating back to a page that references your module will not re-execute it — "Script" will only be logged on the initial load.
This is by design, and in many cases it’s actually ideal.
Since module code runs once, it’s a perfect place to set up event delegation. This avoids a common failure mode with enhanced navigation, where code like the following works on the first load and then mysteriously stops:
document.getElementById('save').addEventListener('click', saveHandler);
After navigation, the #save element is replaced, the original event listener is lost, and the module never runs again to reattach it.
With event delegation, the listener is attached to a stable ancestor that isn’t replaced during navigation, so it continues to work as new DOM is rendered:
document.addEventListener('click', e => {
if (!e.target.closest('[data-action="delete"]')) return;
// Handle click.
});
One thing to watch out for with this approach is handler bleed-through. If multiple pages reuse the same class names or data attributes, it’s easy for an event handler to fire on the “wrong” page.
To avoid this, you can scope the handler to a specific page or layout root before continuing:
document.addEventListener('click', e => {
if (!e.target.closest('[data-page="orders"]')) return;
if (!e.target.closest('[data-action="delete"]')) return;
// Handle click for the Orders page only.
});
By scoping to a page-level element, you ensure that delegated handlers only run when the interaction occurs within the intended page, even as enhanced navigation swaps content in and out.
So while the MutationObserver technique works well for standard scripts, module scripts are best treated as one-time initializers, making event delegation—with proper scoping—the approach you’re likely looking for when working with enhanced navigation.
If you truly need per-page execution semantics, you’ll need to fall back to the <page-script> web component approach.