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? #

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 textStep 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’slayouts/over files inside the theme, so you don’t need to modify theme files directly.
Version with Multilanguage (Recommended) #
<!-- 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:
-
Copy the file from the theme to your project:
cp themes/blowfish/layouts/_default/baseof.html layouts/_default/baseof.html -
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 notificationaria-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.