Loading and error page

Rationale

JHipster generates a default index.html that serves as the shell page for Angular single-page applications. This page is served by the Spring Gateway and is visible to users in two scenarios:

  1. While the application loads — the page displays a loading animation until Angular bootstraps and takes over the DOM.

  2. When the application fails to load — if JavaScript bundles fail to download (typically due to network issues), the page shows a fallback error message after a timeout.

The default JHipster page is unsuitable for production because:

  • The loading animation is a novelty pacman graphic.

  • The error fallback references developer tooling (npm install, ./mvnw, JHipster Stack Overflow, Gitter chat).

  • The <noscript> message is a terse one-liner with no guidance.

Since the page is served as a static resource by the Gateway, only inline HTML, CSS referenced in <head>, and inline JavaScript are available when Angular bundles fail to load. The replacement must be entirely self-contained in index.html and loading.css.

What changed

The replacement introduces three visual states:

State Description

Loading

A clean circular spinner centred on the page with "Loading…​" text. Replaces the pacman animation.

Error fallback

A friendly error card with a warning icon, plain-language explanation, numbered troubleshooting steps, a "Refresh page" button, and a support email link. Replaces the JHipster developer error block.

Noscript

A styled message explaining that JavaScript is required, with a support contact. Replaces the bare <h1> tag.

Auto-retry logic

The timeout script uses sessionStorage to implement a single silent retry:

  1. After 8 seconds, if Angular has not bootstrapped, check sessionStorage for a retry flag.

  2. If no flag exists, set it and call location.reload() — this handles transient network failures silently.

  3. If the flag is already present (second attempt), remove it, hide the spinner, and display the error fallback.

This avoids showing the error on brief network blips while ensuring the user is not stuck in an infinite reload loop.

Files to modify

Two files in each JHipster application require replacement:

File Purpose

src/main/webapp/content/css/loading.css

Spinner animation and error fallback styles.

src/main/webapp/index.html

The <body> content: spinner, error card, noscript block, and timeout script.

The <head> section of index.html is application-specific (title, favicon paths, meta tags) and should be preserved.

Template: loading.css

Replace the entire contents of loading.css with:

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* Loading spinner */

.app-loading {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  padding: 2rem 1rem;
  color: #333;
}

.app-spinner {
  width: 48px;
  height: 48px;
  border: 4px solid #e0e0e0;
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

.app-loading-text {
  margin-top: 1.5rem;
  font-size: 1rem;
  color: #666;
}

/* Error fallback */

.app-error {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
  padding: 2rem 1rem;
  color: #333;
}

.app-error-card {
  max-width: 520px;
  width: 100%;
  text-align: center;
}

.app-error-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto 1.5rem;
  border-radius: 50%;
  background: #fef2f2;
  display: flex;
  align-items: center;
  justify-content: center;
}

.app-error-icon svg {
  width: 28px;
  height: 28px;
  color: #dc2626;
}

.app-error h1 {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0 0 0.5rem;
  color: #111;
}

.app-error p {
  font-size: 0.95rem;
  line-height: 1.6;
  margin: 0 0 1rem;
  color: #555;
}

.app-error-steps {
  text-align: left;
  margin: 1.25rem 0;
  padding: 1rem 1.25rem;
  background: #f8fafc;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
}

.app-error-steps h2 {
  font-size: 0.85rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #64748b;
  margin: 0 0 0.75rem;
}

.app-error-steps ol {
  margin: 0;
  padding-left: 1.25rem;
}

.app-error-steps li {
  font-size: 0.9rem;
  line-height: 1.6;
  color: #444;
  margin-bottom: 0.25rem;
}

.app-error-steps li:last-child {
  margin-bottom: 0;
}

.app-error-retry {
  display: inline-block;
  margin-top: 1.25rem;
  padding: 0.6rem 1.75rem;
  background: #3b82f6;
  color: #fff;
  font-size: 0.95rem;
  font-weight: 500;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.app-error-retry:hover {
  background: #2563eb;
}

.app-error-contact {
  margin-top: 1.5rem;
  font-size: 0.85rem;
  color: #888;
}

.app-error-contact a {
  color: #3b82f6;
  text-decoration: none;
}

.app-error-contact a:hover {
  text-decoration: underline;
}

Template: index.html body

Replace the <body> section of index.html with the following. Keep the existing <head> section unchanged.

  <body>
    <jhi-main>
      <!-- Loading spinner — visible until Angular bootstraps -->
      <div class="app-loading">
        <div class="app-spinner"></div>
        <div class="app-loading-text">Loading...</div>
      </div>
      <!-- Error fallback — shown if the app fails to load -->
      <div id="app-error" class="app-error">
        <div class="app-error-card">
          <div class="app-error-icon">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
            </svg>
          </div>
          <h1>We're having trouble loading this page</h1>
          <p>This is usually caused by a slow or unstable internet connection.</p>
          <div class="app-error-steps">
            <h2>You can try</h2>
            <ol>
              <li>Check that you are connected to the internet</li>
              <li>Refresh the page using the button below</li>
              <li>Clear your browser cache with <strong>Ctrl+Shift+R</strong> (or <strong>Cmd+Shift+R</strong> on Mac)</li>
              <li>Wait a few minutes and try again</li>
            </ol>
          </div>
          <button class="app-error-retry" onclick="location.reload()">Refresh page</button>
          <div class="app-error-contact">
            Still not working? Contact us at
            <a href="mailto:[email protected]">[email protected]</a> (1)
          </div>
        </div>
      </div>
    </jhi-main>
    <noscript>
      <div class="app-error" style="display: flex;">
        <div class="app-error-card">
          <div class="app-error-icon">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
            </svg>
          </div>
          <h1>JavaScript is required</h1>
          <p>This page needs JavaScript to work. Please enable JavaScript in your browser settings and refresh the page.</p>
          <div class="app-error-contact">
            Need help? Contact us at
            <a href="mailto:[email protected]">[email protected]</a> (1)
          </div>
        </div>
      </div>
    </noscript>
    <script type="text/javascript">
      // If the app hasn't loaded after 8 seconds, try one silent reload.
      // If it still fails after the retry, show the error message.
      window.onload = function () {
        setTimeout(function () {
          var retried = sessionStorage.getItem('app-load-retry');
          if (!retried) {
            sessionStorage.setItem('app-load-retry', '1');
            location.reload();
          } else {
            sessionStorage.removeItem('app-load-retry');
            var loading = document.querySelector('.app-loading');
            var error = document.getElementById('app-error');
            if (loading) loading.style.display = 'none';
            if (error) error.style.display = 'flex';
          }
        }, 8000);
      };
    </script>
  </body>
1 Replace [email protected] with the appropriate support email address for your application.

Applying to a new JHipster application

Follow these steps after generating a new JHipster project:

  1. Replace loading.css — Copy the loading.css template into src/main/webapp/content/css/loading.css, overwriting the generated file.

  2. Update index.html — Open src/main/webapp/index.html and:

    1. Keep the entire <head> section (title, meta tags, favicons, stylesheets).

    2. Replace everything inside <body>…​</body> with the body template.

    3. Update the support email address in both the error fallback and noscript sections.

  3. Remove pacman references — The old loading.css referenced images like logo-jhipster.png or logo-blk.svg in the pacman animation. These image files can be removed if they are not used elsewhere.

  4. Test the three states:

    1. Loading — Open the application normally and verify the spinner appears briefly before Angular loads.

    2. Error fallback — Use browser DevTools to block JavaScript loading (Network tab, block *.js), then reload. After ~16 seconds (8s + retry + 8s), the error card should appear.

    3. Noscript — Disable JavaScript in browser settings and reload. The JavaScript-required message should display.

Applied to

This replacement has been applied to the following applications:

  • registration-portal (Event Portal)

  • admin-ui (Event Admin)

  • membership-ui (Membership Portal)