As of
Est.Ā 

Elimination of Errors after Deployment ā€‹

šŸ’”You don't want pages to break when updating the website. But that's exactly what happened to the jotter.

Errors after Rebuilding VitePress ā€‹

When I released the first updates during the QA phase of Ergberg's Jotter, I got crippled pages.

hydration errorhydration error

Crippled HTML after hydration error

A look at the console shows: app.278b95ed.js:1 Hydration completed but contains mismatches. Yes, that's exactly what it looks like.

Cause of the Loading Errors ā€‹

VitePress is prepared for errors caused by redeployments. These errors will occur only under rare conditions.

  1. This happens only in production, not in development mode.
  2. You visit a VitePress site.
  3. The site gets rebuilt while you visit.
  4. After redeployment, you navigate to a page that was not previously preloaded / cached.
  5. The build created a new version of that page, so its filename changed and it can no longer be found under the old name.

When you reach the last item in the list, clicking the link would result in a "Page not found" error. Before we look at VitePress's strategy for avoiding these errors, I want to explain why this doesn't happen too often:

These errors only occur when you navigate from one page of the site to another. They do not occur when you explicitly load the HTML page. Explicit loads can be triggered in many ways:

  • You navigate to the website from an external location,
  • You enter the URL into the browser's address bar,
  • You press the reload button or the reload shortcut,
  • You launch an installed PWA

Most target pages are in your cache: in production, VitePress pre-fetches the targets of all internal links are visible on a page. As you scroll down, more and more pages are preloaded and end up in your browser's cache. When the sidebar navigation is visible, this automatically loads many pages into the cache. There is a good chance that only pages from the cache will be accessed as you stroll around the site.

Watch Out!

If you have visited all pages of a VitePress site and clicked all buttons, you may miss further updates to the site. This is not just because the files end up in the browser's cache. It's also a result of the way Vue components are rendered: As you navigate between pages on the site, JavaScript files are requested for the target pages. Once imported, they will not be reimported again.[1] Even if the JavaScript file is removed from the cache after import, it is not fetched again from the server on subsequent visits of the page.

A valid solution to avoid the loading problems is to cache all files using a service worker. Every new build should change the service worker script. That change triggers a reinstall of the service worker as soon as the page is (force) refreshed. Then the caches can be updated automatically. But as you can not be sure that service workers are supported on all targets, this will not be a general solution.

VitePress's Strategy to Avoid Errors ā€‹

VitePress's router detects load errors. If a page cannot be found, it explicitly loads information about the possible new name of the missing page and tries to reload it with that new name.

This sounds good. Why didn't it work in my case? I suspect the mechanism fails when not only the content pages change, but also the generated VitePress app.js file. The rest of this jotting provides some details that led me to this suspicion and how I dealt with it.

Cache Busting Using Hashes ā€‹

The JavaScript files under .vitepress/dist/assets all have names with hashes. When the contents of a file change, the hash and filename also change as well. This happens often when the site is rebuilt.

The advantage of this cache-busting scheme is that files never change but are simply replaced by new version with different names. Consequently, JavaScript files can be cached for long time without revalidation. VitePress sends files in its /assets/ directory with Cache-Control public,max-age=31536000,immutable. This means that asset files can go to a public cache, stay fresh for one year, and will not change in the future. Files outside the /assets/ directory, especially HTML pages, have Cache-Control no-cache. This means that they must be revalidated before the cached version can be used.

Files Generated by VitePress ā€‹

When VitePress navigates a link to another internal page, it does not load the HTML file of that page. Instead, it runs the JavaScript file that renders the page. There are two version of such JavaScript files for a page: the normal and the lean version.

File NameDescription
page.htmlThe HTML file that renders the static view of the page and than runs page.<hash>.lean.js for hydration.
page.<hash>.lean.jsThe JavaScript file that contains only the hydration code for the page.
page.<hash>.jsLike page.<hash>.lean.js, but also has the JavaScript code for rendering the static part of the page.

A link on a VitePress page that points to another VitePress page does not contain a hash value. The hash is added by the router using a function called pathToFile. The hash values for the pages come from a mapping called __VP_HASH_MAP__. It is included in each HTML page and in a file called hashmap.json.

If a file can't be found, VitePress assumes that this is because of hash changes. Therefore, it updates __VP_HASH_MAP__ by reloading hashmap.json. Then it tries again to load the file with the updated hash value.

The Failing Scenario ā€‹

The following list shows how the files are loaded. At the beginning, the page with URL /overview is shown. Then the site is rebuilt, but the browser does not know yet. If you click on a link with href="/basics/", the current page resolves it to /assets/basics_index.md.b78be724.js, using the latest version of __VP_HASH_MAP__. Since this URL is not found on the server (anymore), the mapping is updated by reading the hashmap.json file. Then, the new version /assets/basics_index.md.b66e42dd.js is requested and found.

txt
app.4151cbfa.js ...
(200) localhost:5000/assets/overview.md.c71d768c.js

(404) localhost:5000/assets/basics_index.md.b78be724.js

(200) localhost:5000/hashmap.json
(200) localhost:5000/assets/basics_index.md.b66e42dd.js
(200) localhost:5000/assets/app.278b95ed.js
(200) localhost:5000/assets/basics_index.md.b66e42dd.lean.js
app.278b95ed.js:1 Hydration completed but contains mismatches.
app.4151cbfa.js ...
(200) localhost:5000/assets/overview.md.c71d768c.js

(404) localhost:5000/assets/basics_index.md.b78be724.js

(200) localhost:5000/hashmap.json
(200) localhost:5000/assets/basics_index.md.b66e42dd.js
(200) localhost:5000/assets/app.278b95ed.js
(200) localhost:5000/assets/basics_index.md.b66e42dd.lean.js
app.278b95ed.js:1 Hydration completed but contains mismatches.

Normally that would be the end of the story and all would be set.

In my case, the new deployment does not only produce a new version of /basics/ (/assets/basics_index.md.b66e42dd.js) but also a new version of the VitePress app, which is imported as /assets/app.278b95ed.js in /assets/basics_index.md.b66e42dd.js.

ListenerRouterapp.4151cbfa.js...md.b66e42dd.jsapp.278b95ed.js...md.b66e42dd.lean.jsfile not foundif not cachedyet and replacedwith new versionby new buildget the nowcurrent hashesthis is the new versionif the app hash changes:this import resultsin a new appinstance whichtries to mountand hydrate itselfHydration fails.go(/basics/)loadPageModule(/basics/index.html)import(/assets/basics_index.md.b78be724.js)fetch(/hashmap.json)import(/assets/basics_index.md.b66e42dd.js)import(/assets/app.278b95ed.js)import(/assets/basics_index.md.b66e42dd.lean.js)hydrate()ListenerRouterapp.4151cbfa.js...md.b66e42dd.jsapp.278b95ed.js...md.b66e42dd.lean.js
Hydration error after VitePress rebuild

This import creates a new instance of the VitePress app in the browser. Initializing the file results in hydration in an inconsistent state of the page and results in a hydration error as shown in .

My Solution ā€‹

For a long time, I was looking for errors in my code that led to this scenario. I assume that my code has unusual properties. It always recreates new versions of the app file due to the highly dynamic nature of the index.<hash>.json and topics.<hash>.json files it imports.

For a while I tried to develop a solution based on service workers. In the end, however, I refrained from using service workers as the only solution because they are not supported on all browsers/constellations.

Reload on Hydration Failures ā€‹

My actual solution to the problem is quite simple: I just replace the error message on hydration errors with a forced reload of the page. To not surprise visitors too much, I also added a warning message:

Your sensitive mind notices a wrongness in the fabric of space.

If you remember this quote from elsewhere, drop me a note šŸ˜.

Fix for Asynchronous Components ā€‹

Reloading the page also helps with a second source of errors. When using asynchronous components, loading JavaScript files is postponed to a later time. If the site is rebuilt before the delayed loading occurs, this can also cause errors if the original version of the file cannot be found any more. Most of the time, these errors occure without any helpful messages. The dynamic behavior simply breaks silently. VitePress does not detect or fix this second source of loading errors.

My fix for asynchronous components is the same as for hydration errors. But instead of replacing the generation of an error message, I define an error handler in my main index.js:

js
export default {
  Layout,
  enhanceApp({ app, router }) {
    ...
    app.config.errorHandler = function (err, vm, info) {
      fetch("/")
        .then(() => {
          if (document.location) {
            alert(
              `\nYour sensitive mind notices a\nwrongness in the fabric of space.\n\nSome things might have changed.`
            );
            document.location.reload(true);
          }
        })
        .catch((err) => {
          alert(`\nYou seem to be offline.`);
          history.back();
        });
    };
  }
}
export default {
  Layout,
  enhanceApp({ app, router }) {
    ...
    app.config.errorHandler = function (err, vm, info) {
      fetch("/")
        .then(() => {
          if (document.location) {
            alert(
              `\nYour sensitive mind notices a\nwrongness in the fabric of space.\n\nSome things might have changed.`
            );
            document.location.reload(true);
          }
        })
        .catch((err) => {
          alert(`\nYou seem to be offline.`);
          history.back();
        });
    };
  }
}

  1. ... at least not until the browser tab is refreshed. ā†©ļøŽ