Skip to main content
  1. Posts/

How to Create Custom Notifications in Hugo Blowfish Theme (and Other Themes)

·1444 words·7 mins·
Noor Khafidzin
Author
Noor Khafidzin
A homelab enthusiast obsessed with system efficiency and the art of troubleshooting.
Table of Contents

Why Do You Need Custom Notifications in Hugo?
#

When you migrate a blog — for example from Blogger, WordPress, or another platform to Hugo — there’s one thing that’s often overlooked: informing your readers.

Old readers might still be accessing old URLs. Your SEO is in transition. And without a notification, they don’t know what’s happening.

Hugo doesn’t provide a built-in notification component. But with its flexible partial template system, you can build one yourself — and the result is clean, lightweight, and easy to customize.


What Are We Building?
#

demo custom notificarion

The notification we’ll create has the following features:

  • Warning banner in yellow with a warning icon
  • Dismiss button — once closed, it won’t appear again (uses localStorage)
  • Fade-in animation when the page loads
  • ✅ Full dark mode support via Tailwind CSS
  • Multilanguage support via Hugo i18n (optional)
  • Compatible with the Blowfish theme and other Hugo themes

Required File Structure
#

layouts/
  partials/
    notification.html       ← main partial notification file
i18n/
  en.yaml                   ← English notification text
  id.yaml                   ← Indonesian notification text

Step 1: Create the Notification Partial File
#

Create a new file at layouts/partials/notification.html.

Note: The layouts/ folder lives at the root of your Hugo project, not inside the theme folder. This is important — Hugo prioritizes files in the project’s layouts/ over files inside the theme, so you don’t need to modify theme files directly.

<!-- layouts/partials/notification.html -->
<div class="flex pb-2" id="migration-notice-wrapper">
  <div
    id="migration-notice"
    class="bg-yellow-100 dark:bg-yellow-900 border border-yellow-300 dark:border-yellow-700 text-yellow-800 dark:text-yellow-200 p-4 rounded-lg shadow-xl flex items-start space-x-3 transition-all duration-300 ease-in-out transform max-w-xl"
    role="alert"
    style="display: none; opacity: 0; margin: 0 auto;"
  >
    <!-- Warning Icon -->
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.3 16c-.77 1.333.192 3 1.732 3z" />
    </svg>

    <!-- Text Content -->
    <div class="flex-grow">
      <p class="font-bold text-sm">{{ i18n "migration_notice" | safeHTML }}</p>
      <p class="text-sm">{{ i18n "migration_notice_details" | safeHTML }}</p>
    </div>

    <!-- Close Button -->
    <button
      id="dismiss-notice"
      type="button"
      class="text-yellow-800 dark:text-yellow-200 hover:text-yellow-900 dark:hover:text-yellow-100 transition duration-150 ease-in-out p-1 -m-1 rounded-full focus:outline-none focus:ring-2 focus:ring-yellow-500"
      aria-label="Close notification"
    >
      <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
        <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
      </svg>
    </button>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const notice = document.getElementById('migration-notice');
  const dismissBtn = document.getElementById('dismiss-notice');
  
  // Check if user has previously dismissed the notification
  const isDismissed = localStorage.getItem('migrationNoticeDismissed');
  
  if (!isDismissed) {
    // Show with fade-in animation after 500ms
    setTimeout(() => {
      notice.style.display = 'flex';
      setTimeout(() => {
        notice.style.opacity = '1';
      }, 10);
    }, 500);
  }

  // Handle dismiss button click
  dismissBtn.addEventListener('click', () => {
    notice.style.opacity = '0';
    setTimeout(() => {
      notice.style.display = 'none';
      // Save dismiss status to localStorage
      localStorage.setItem('migrationNoticeDismissed', 'true');
    }, 300);
  });
});
</script>

Version Without Multilanguage (Simple)
#

If your blog is single-language, use this version — it’s simpler and doesn’t require i18n files:

<!-- layouts/partials/notification.html -->
<div class="flex pb-2" id="migration-notice-wrapper">
  <div
    id="migration-notice"
    class="bg-yellow-100 dark:bg-yellow-900 border border-yellow-300 dark:border-yellow-700 text-yellow-800 dark:text-yellow-200 p-4 rounded-lg shadow-xl flex items-start space-x-3 transition-all duration-300 ease-in-out transform max-w-xl"
    role="alert"
    style="display: none; opacity: 0; margin: 0 auto;"
  >
    <!-- Warning Icon -->
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.3 16c-.77 1.333.192 3 1.732 3z" />
    </svg>

    <!-- Text Content — replace as needed -->
    <div class="flex-grow">
      <p class="font-bold text-sm">Site is currently under migration.</p>
      <p class="text-sm">Some pages may not be available yet. Thank you for your understanding.</p>
    </div>

    <!-- Close Button -->
    <button
      id="dismiss-notice"
      type="button"
      class="text-yellow-800 dark:text-yellow-200 hover:text-yellow-900 dark:hover:text-yellow-100 transition duration-150 ease-in-out p-1 -m-1 rounded-full focus:outline-none focus:ring-2 focus:ring-yellow-500"
      aria-label="Close notification"
    >
      <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
        <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
      </svg>
    </button>
  </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', () => {
  const notice = document.getElementById('migration-notice');
  const dismissBtn = document.getElementById('dismiss-notice');
  
  const isDismissed = localStorage.getItem('migrationNoticeDismissed');
  
  if (!isDismissed) {
    setTimeout(() => {
      notice.style.display = 'flex';
      setTimeout(() => { notice.style.opacity = '1'; }, 10);
    }, 500);
  }

  dismissBtn.addEventListener('click', () => {
    notice.style.opacity = '0';
    setTimeout(() => {
      notice.style.display = 'none';
      localStorage.setItem('migrationNoticeDismissed', 'true');
    }, 300);
  });
});
</script>

Step 2: Insert the Partial into baseof.html (Blowfish Theme)
#

If You Don’t Already Have baseof.html in layouts/
#

The Blowfish theme stores baseof.html inside the theme folder (themes/blowfish/layouts/_default/baseof.html). You should not edit files inside the theme folder directly — changes will be lost when the theme is updated.

The correct approach:

  1. Copy the file from the theme to your project:

    cp themes/blowfish/layouts/_default/baseof.html layouts/_default/baseof.html
  2. Create the folder if it doesn’t exist:

    mkdir -p layouts/_default

Hugo will automatically use the version in your layouts/ folder.

Add the Partial to baseof.html
#

Find this section in layouts/_default/baseof.html:

{{ $header := print "header/" site.Params.header.layout ".html" }}
{{ if templates.Exists ( printf "partials/%s" $header ) }}
  {{ partial $header . }}
{{ else }}
  {{ partial "header/basic.html" . }}
{{ end }}

Add one line below it:

{{ $header := print "header/" site.Params.header.layout ".html" }}
{{ if templates.Exists ( printf "partials/%s" $header ) }}
  {{ partial $header . }}
{{ else }}
  {{ partial "header/basic.html" . }}
{{ end }}

{{/* migration-notice */}}
{{ partial "notification.html" . }}

For Other Hugo Themes
#

Find baseof.html in the theme you’re using, then insert the partial right after the <header> tag or after the header partial block finishes rendering. The principle is the same: the notification is displayed below the header, before the main content.

Common example for other themes:

{{ partial "header.html" . }}

{{/* Custom notification */}}
{{ partial "notification.html" . }}

<main>
  {{ block "main" . }}{{ end }}
</main>

Step 3: Setup Multilanguage (Optional)
#

If you’re using Hugo with multiple languages, add the notification text to the i18n files.

i18n/en.yaml
#

- id: "migration_notice"
  translation: "Site is under migration from Blogger to Hugo."

- id: "migration_notice_details"
  translation: "Some pages may be temporarily unavailable. Thank you for your patience."

i18n/id.yaml
#

- id: "migration_notice"
  translation: "Situs sedang dalam migrasi dari Blogger ke Hugo."

- id: "migration_notice_details"
  translation: "Beberapa halaman mungkin belum tersedia sementara. Terima kasih atas pengertianmu."

Hugo will automatically select the appropriate text based on the visitor’s active language.


Feature Explanation: How Does This Work?
#

1. Dismissible with localStorage
#

const isDismissed = localStorage.getItem('migrationNoticeDismissed');

When a user closes the notification, the status is saved to their browser’s localStorage. When the page is refreshed or the user navigates to another page, the notification does not appear again — because Hugo checks localStorage before displaying it.

This is important for UX: you don’t want notifications to keep appearing and annoying your readers.

2. Fade-in Animation
#

setTimeout(() => {
  notice.style.display = 'flex';
  setTimeout(() => {
    notice.style.opacity = '1';
  }, 10);
}, 500);

A 500ms delay before appearing gives the page time to finish loading, so the notification feels smooth — not an abrupt “pop” that startles the user.

3. Dark Mode Support
#

Tailwind classes like dark:bg-yellow-900 and dark:text-yellow-200 automatically activate when the Blowfish theme switches to dark mode. No additional JavaScript is needed for this.

4. Accessibility
#

  • role="alert" tells screen readers that this is an important notification
  • aria-label="Close notification" on the X button ensures assistive technology users can close the notification
  • Focus ring (focus:ring-2) on the dismiss button for keyboard navigation

Customization Tips
#

Change the banner color: Replace the yellow classes with blue, red, green, or another color to match your context:

<!-- Info (blue) -->
class="bg-blue-100 dark:bg-blue-900 border border-blue-300 ..."

<!-- Error/Important (red) -->
class="bg-red-100 dark:bg-red-900 border border-red-300 ..."

<!-- Success (green) -->
class="bg-green-100 dark:bg-green-900 border border-green-300 ..."

Reset the notification for testing: Open the browser console and run:

localStorage.removeItem('migrationNoticeDismissed');

Show the notification only on specific pages: Add a Hugo condition in the partial:

{{ if eq .RelPermalink "/" }}
  <!-- notification only appears on homepage -->
{{ end }}

Conclusion
#

Custom notifications in Hugo don’t require plugins, don’t require heavy JavaScript frameworks, and don’t require modifying theme files directly.

With a single partial file, a few lines of vanilla JavaScript, and optional i18n files — you’ll have a notification system that is:

  • User-friendly (dismissible, smooth animation)
  • Accessible (ARIA attributes)
  • Dark mode supported
  • Easy to customize for other use cases (maintenance, promos, announcements)

Next step: once the migration is complete, simply remove {{ partial "notification.html" . }} from baseof.html — clean with no trace left behind.


Have questions or ran into issues during implementation? Write in the comments section.

Related


Load Comments