From 5cad4d3df92c80a79169577a60750c0abb5fe793 Mon Sep 17 00:00:00 2001 From: Cell <1024@lruihao.cn> Date: Wed, 24 Jun 2026 12:43:37 +0800 Subject: [PATCH] refactor(assets): PWA improvements and app config snake_case migration (#790) - Service worker: versioned cache name, navigation preload, stale-while-revalidate for images - New PWAModule extracted from MiscModule with async registration and update detection - SW update notification toast in Hugo template with i18n support (16 languages) - Web app manifest template (site.webmanifest) generated from [params.app] config - [params.app] keys migrated to snake_case: name, short_name, pwa, no_favicon, svg_favicon, mask_color, tile_color, theme_color - enablePWA moved from [params] to [params.app].pwa - theme_color no longer accepts single-value, only {light, dark} map - EventBus: new fixit:sw-update event - SCSS: new _sw-toast.scss widget for update notification --- assets/js/core/event-bus.ts | 1 + assets/js/core/public-api.ts | 3 + assets/js/core/tokens.ts | 7 +- assets/js/main.ts | 1 + assets/js/modules/misc.ts | 20 ----- assets/js/modules/pwa.ts | 72 +++++++++++++++++ assets/js/service-worker.template.js | 79 +++++++++++++++---- .../scss/{custom.scss => custom.scss.example} | 47 +++++------ assets/scss/widgets/_index.scss | 1 + assets/scss/widgets/_sw-toast.scss | 72 +++++++++++++++++ hugo.toml | 55 +++++++++---- i18n/de.toml | 7 ++ i18n/en.toml | 7 ++ i18n/es.toml | 7 ++ i18n/fr.toml | 7 ++ i18n/hi.toml | 7 ++ i18n/it.toml | 7 ++ i18n/ja.toml | 7 ++ i18n/ko.toml | 7 ++ i18n/pl.toml | 7 ++ i18n/pt-BR.toml | 7 ++ i18n/ro.toml | 7 ++ i18n/ru.toml | 7 ++ i18n/sr.toml | 7 ++ i18n/vi.toml | 7 ++ i18n/zh-CN.toml | 7 ++ i18n/zh-TW.toml | 7 ++ layouts/_partials/base/assets.html | 6 +- layouts/_partials/base/head/index.html | 24 +++--- layouts/_partials/base/widgets.html | 11 +++ layouts/home.manifest.webmanifest | 30 +++++++ 31 files changed, 450 insertions(+), 91 deletions(-) create mode 100644 assets/js/modules/pwa.ts rename assets/scss/{custom.scss => custom.scss.example} (75%) create mode 100644 assets/scss/widgets/_sw-toast.scss create mode 100644 layouts/home.manifest.webmanifest diff --git a/assets/js/core/event-bus.ts b/assets/js/core/event-bus.ts index 85d9e888..5284364a 100644 --- a/assets/js/core/event-bus.ts +++ b/assets/js/core/event-bus.ts @@ -7,6 +7,7 @@ export interface FixItEventMap { 'fixit:partial-decrypted': { target: Element } 'fixit:re-encrypt': void 'fixit:code-tab-sync': { lang: string, source: HTMLElement } + 'fixit:sw-update': void } /** Document event map augmented with FixIt custom events. */ diff --git a/assets/js/core/public-api.ts b/assets/js/core/public-api.ts index 21f69466..3326ce3b 100644 --- a/assets/js/core/public-api.ts +++ b/assets/js/core/public-api.ts @@ -6,6 +6,7 @@ import { EncryptionModule } from '../modules/encryption' import { EventsModule } from '../modules/events' import { MenuModule } from '../modules/menu' import { MiscModule } from '../modules/misc' +import { PWAModule } from '../modules/pwa' import { SearchModule } from '../modules/search' import { ThemeModule } from '../modules/theme' import { TocModule } from '../modules/toc' @@ -24,6 +25,7 @@ export class PublicAPI implements FixItPublicAPI { readonly menu readonly search readonly enc + readonly pwa readonly misc readonly content readonly events @@ -38,6 +40,7 @@ export class PublicAPI implements FixItPublicAPI { this.menu = new MenuModule(this.core) this.search = new SearchModule(this.core) this.enc = new EncryptionModule(this.core) + this.pwa = new PWAModule(this.core) this.misc = new MiscModule(this.core) this.content = new ContentModule(this.core, this.code) this.events = new EventsModule(this.core, this.toc, this.code) diff --git a/assets/js/core/tokens.ts b/assets/js/core/tokens.ts index ac604861..ecdfee38 100644 --- a/assets/js/core/tokens.ts +++ b/assets/js/core/tokens.ts @@ -67,7 +67,6 @@ export interface ContentService { // ─── MiscService ─── export interface MiscService { initSiteTime: () => void - initServiceWorker: () => void initAutoMark: () => void initReward: () => void initPostChatUser: () => void @@ -75,6 +74,11 @@ export interface MiscService { setup: () => void } +// ─── PWAService ─── +export interface PWAService { + setup: () => void +} + // ─── EventsService ─── export interface EventsService { onScroll: () => void @@ -98,6 +102,7 @@ export interface FixItPublicAPI { readonly menu: MenuService readonly search: SearchService readonly enc: EncryptionService + readonly pwa: PWAService readonly misc: MiscService readonly content: ContentService readonly events: EventsService diff --git a/assets/js/main.ts b/assets/js/main.ts index 08f2e72a..9bb425df 100644 --- a/assets/js/main.ts +++ b/assets/js/main.ts @@ -28,6 +28,7 @@ function bootstrap(): void { window.fixit.search.setup() window.fixit.content.setup() window.fixit.enc.setup() + window.fixit.pwa.setup() window.fixit.misc.setup() window.fixit.events.setup() } diff --git a/assets/js/modules/misc.ts b/assets/js/modules/misc.ts index bf67cea7..13a8a182 100644 --- a/assets/js/modules/misc.ts +++ b/assets/js/modules/misc.ts @@ -49,25 +49,6 @@ export class MiscModule implements MiscService { } } - /** Register the service worker for PWA support. */ - initServiceWorker() { - if (this.core.config.PWA?.enable && 'serviceWorker' in navigator) { - navigator.serviceWorker - .register(this.core.config.PWA.serviceWorkerURL) - .then((_registration) => { - // console.log('Service Worker Registered'); - }) - .catch((error) => { - console.error('error: ', error) - }) - navigator.serviceWorker - .ready - .then((_registration) => { - // console.log('Service Worker Ready'); - }) - } - } - /** Save and restore scroll position as an automatic bookmark. */ initAutoMark() { if (!this.core.config.autoBookmark) @@ -144,7 +125,6 @@ export class MiscModule implements MiscService { /** Initialize all miscellaneous features. */ setup() { this.initSiteTime() - this.initServiceWorker() this.initAutoMark() this.initReward() this.initPostChatUser() diff --git a/assets/js/modules/pwa.ts b/assets/js/modules/pwa.ts new file mode 100644 index 00000000..58bab539 --- /dev/null +++ b/assets/js/modules/pwa.ts @@ -0,0 +1,72 @@ +import type { CoreService, PWAService } from '../core/tokens' +import { eventBus } from '../core/event-bus' + +/** + * PWA module — service worker registration and update notification. + * + * Responsibilities: + * - Register service worker with update detection. + * - Periodically check for service worker updates. + * - Show update notification toast and handle refresh. + */ +export class PWAModule implements PWAService { + constructor(private readonly core: CoreService) {} + + /** Register the service worker with update detection and notification binding. */ + async setup() { + const pwa = this.core.config.PWA + if (!pwa?.enable || !('serviceWorker' in navigator)) + return + + const registration = await navigator.serviceWorker + .register(pwa.serviceWorkerURL) + .catch((error: unknown) => { + console.error('Service Worker registration failed:', error) + return null + }) + + if (!registration) + return + + // Check for updates periodically (every hour) + setInterval(() => registration.update(), 3600000) + + // Listen for new service worker installing + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing + if (!newWorker) + return + + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + eventBus.emit('fixit:sw-update') + } + }) + }) + + // Reload when a new service worker takes control + navigator.serviceWorker.addEventListener('controllerchange', () => { + window.location.reload() + }) + + this.bindUpdateNotification(registration) + } + + /** + * Bind the template-rendered update notification toast. + * Shows the toast on `fixit:sw-update` and wires the refresh button to skip waiting. + */ + private bindUpdateNotification(registration: ServiceWorkerRegistration) { + const toast = document.querySelector('.sw-update-notification') + if (!toast) + return + + eventBus.on('fixit:sw-update', () => { + toast.querySelector('.sw-update-btn')!.addEventListener('click', () => { + registration.waiting?.postMessage({ type: 'SKIP_WAITING' }) + toast.classList.remove('visible') + }, { once: true }) + requestAnimationFrame(() => toast.classList.add('visible')) + }) + } +} diff --git a/assets/js/service-worker.template.js b/assets/js/service-worker.template.js index e1474fda..93c85d82 100644 --- a/assets/js/service-worker.template.js +++ b/assets/js/service-worker.template.js @@ -2,8 +2,9 @@ /** * Service Worker - * @description Two-strategy caching with the native Cache API: + * @description Three-strategy caching with the native Cache API: * - Static assets (fingerprinted CSS/JS): cache-first, immutable + * - Images: stale-while-revalidate * - HTML pages: network-first, fallback to cache when offline * Asset URLs are injected at build time via Hugo's ExecuteAsTemplate, * ensuring they match fingerprinted paths when fingerprint is enabled. @@ -12,7 +13,7 @@ /* ========== Constants ========== */ -const CACHE_NAME = 'fixit-v1' +const CACHE_NAME = 'fixit-{{ .version }}' /** URLs to pre-cache during the install event (injected by Hugo). */ const PRECACHE_URLS = [ @@ -30,7 +31,10 @@ const NOT_FOUND_PAGE = '{{ .relURL }}404.html' const MAX_HTML_CACHE_ENTRIES = 100 /** Static asset extensions — these are fingerprinted by Hugo and safe to cache forever. */ -const STATIC_EXTENSIONS = ['css', 'js', 'woff2', 'woff', 'ttf', 'eot', 'svg', 'webp', 'avif', 'png', 'jpg', 'jpeg', 'gif', 'ico'] +const STATIC_EXTENSIONS = ['css', 'js', 'woff2', 'woff', 'ttf', 'eot', 'svg'] + +/** Image extensions — use stale-while-revalidate strategy. */ +const IMAGE_EXTENSIONS = ['webp', 'avif', 'png', 'jpg', 'jpeg', 'gif', 'ico'] /* ========== Helpers ========== */ @@ -52,6 +56,20 @@ function isStaticAsset(url) { return STATIC_EXTENSIONS.includes(ext) } +/** + * Check if a URL is a same-origin image asset. + */ +function isImageAsset(url) { + if (!url.startsWith(self.location.origin)) + return false + const pathname = new URL(url).pathname + const dotIndex = pathname.lastIndexOf('.') + if (dotIndex === -1) + return false + const ext = pathname.slice(dotIndex + 1).toLowerCase() + return IMAGE_EXTENSIONS.includes(ext) +} + /** * Evict oldest HTML cache entries when the limit is exceeded. * Operates on cache insertion order (Cache API preserves insertion order for keys()). @@ -62,7 +80,7 @@ async function evictOldEntries() { const keys = await cache.keys() // PRECACHE_URLS contains relative paths from Hugo; resolve them to absolute URLs for comparison. const precacheSet = new Set(PRECACHE_URLS.map(url => new URL(url, self.location.origin).href)) - const htmlKeys = keys.filter(req => !isStaticAsset(req.url) && !precacheSet.has(req.url)) + const htmlKeys = keys.filter(req => !isStaticAsset(req.url) && !isImageAsset(req.url) && !precacheSet.has(req.url)) const excess = htmlKeys.length - MAX_HTML_CACHE_ENTRIES if (excess > 0) { await Promise.all(htmlKeys.slice(0, excess).map(req => cache.delete(req))) @@ -82,13 +100,17 @@ self.addEventListener('install', (event) => { ) }) -/** Clean up old caches, evict stale HTML entries, and take control immediately. */ +/** Clean up old caches, evict stale HTML entries, enable navigation preload, and take control. */ self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys() - .then(keys => Promise.all( - keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)), - )) + Promise.all([ + caches.keys() + .then(keys => Promise.all( + keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)), + )), + // Enable navigation preload for faster page loads + self.registration.navigationPreload?.enable(), + ]) .then(() => evictOldEntries()) .then(() => self.clients.claim()), ) @@ -104,11 +126,15 @@ self.addEventListener('fetch', (event) => { if (request.method !== 'GET' || !request.url.startsWith(self.location.origin)) return - event.respondWith( - isStaticAsset(request.url) - ? cacheFirst(request) - : networkFirst(request), - ) + if (isStaticAsset(request.url)) { + event.respondWith(cacheFirst(request)) + } + else if (isImageAsset(request.url)) { + event.respondWith(staleWhileRevalidate(request)) + } + else { + event.respondWith(networkFirst(request, event)) + } }) /** @@ -134,13 +160,34 @@ async function cacheFirst(request) { } } +/** + * Stale-while-revalidate strategy for images. + * Returns cached version immediately while fetching an update in the background. + */ +async function staleWhileRevalidate(request) { + const cache = await cachePromise + const cached = await cache.match(request) + + const fetchPromise = fetch(request).then((response) => { + if (response.ok) { + cache.put(request, response.clone()) + } + return response + }).catch(() => cached) + + return cached || fetchPromise +} + /** * Network-first strategy for HTML pages. + * Uses navigation preload when available, falls back to network fetch. * Always tries the network for fresh content, falls back to cache on failure. */ -async function networkFirst(request) { +async function networkFirst(request, event) { try { - const response = await fetch(request) + // Use preloaded response if available (enabled in activate handler) + const preloadResponse = await event?.preloadResponse + const response = preloadResponse || await fetch(request) if (response.ok) { const cache = await cachePromise cache.put(request, response.clone()) diff --git a/assets/scss/custom.scss b/assets/scss/custom.scss.example similarity index 75% rename from assets/scss/custom.scss rename to assets/scss/custom.scss.example index 832df5f2..961c102b 100644 --- a/assets/scss/custom.scss +++ b/assets/scss/custom.scss.example @@ -1,10 +1,11 @@ /// ========================================================================== /// Custom Styles /// -/// Copy this file to your project's assets/scss/ directory to activate it. +/// Copy this file to your project's assets/scss/ directory +/// and rename it to custom.scss to activate it. /// See: https://fixit.lruihao.cn/documentation/advanced/#style-customization /// ========================================================================== -// @use "core/mixins" as *; +@use "core/mixins" as *; // ———————————————————————————————————————————————————————————— // Custom Fonts // Configure font families via [params.appearance] in hugo.toml: @@ -12,25 +13,25 @@ // code_font_family = "Fira Mono, monospace" // Import custom font CSS below (skip if using system fonts). // ———————————————————————————————————————————————————————————— -// @import url('https://chinese-fonts-cdn.deno.dev/packages/lxgwwenkai/dist/LXGWWenKai-Regular/result.css'); -// @import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,700&display=swap&subset=latin-ext'); +@import url('https://chinese-fonts-cdn.deno.dev/packages/lxgwwenkai/dist/LXGWWenKai-Regular/result.css'); +@import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,700&display=swap&subset=latin-ext'); // ———————————————————————————————————————————————————————————— // Custom Page Width // Set pageStyle="custom" in the element to apply. // ———————————————————————————————————————————————————————————— -// @include page-style('custom') { -// @include media('xl') { -// width: ROUND(70%, 2px); -// max-width: 1600px; -// } -// @include media('lg') { -// width: ROUND(60%, 2px); -// } -// @include media('md') { -// width: ROUND(56%, 2px); -// } -// } +@include page-style('custom') { + @include media('xl') { + width: ROUND(70%, 2px); + max-width: 1600px; + } + @include media('lg') { + width: ROUND(60%, 2px); + } + @include media('md') { + width: ROUND(56%, 2px); + } +} // ———————————————————————————————————————————————————————————— // Custom Admonitions @@ -39,9 +40,9 @@ // ban = "fa-solid fa-ban" // Then use in content: {{}} or > [!ban] // ———————————————————————————————————————————————————————————— -// .admonition { -// @include admonition(ban, #ff3d00, rgba(255, 61, 0, 0.1)); -// } +.admonition { + @include admonition(ban, #ff3d00, rgba(255, 61, 0, 0.1)); +} // ———————————————————————————————————————————————————————————— // Custom Task List Style @@ -49,7 +50,7 @@ // [params.taskList] // tip = "fa-regular fa-lightbulb" // ———————————————————————————————————————————————————————————— -// li[data-task='tip'] { -// @include task-icon(#EA9E36); -// @include task-text(#9974F7); -// } +li[data-task='tip'] { + @include task-icon(#EA9E36); + @include task-text(#9974F7); +} diff --git a/assets/scss/widgets/_index.scss b/assets/scss/widgets/_index.scss index 35d68d92..3b5a9381 100644 --- a/assets/scss/widgets/_index.scss +++ b/assets/scss/widgets/_index.scss @@ -8,3 +8,4 @@ @use "noscript-warning"; @use "reading-progress"; @use "scrollbar"; +@use "sw-toast"; diff --git a/assets/scss/widgets/_sw-toast.scss b/assets/scss/widgets/_sw-toast.scss new file mode 100644 index 00000000..e5197cbc --- /dev/null +++ b/assets/scss/widgets/_sw-toast.scss @@ -0,0 +1,72 @@ +@use "core/functions" as *; +@use "core/mixins" as *; + +.sw-update-notification { + position: fixed; + inset-inline-end: 1.5rem; + bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem 1rem; + padding: 0.625rem 1rem; + background-color: fi-var(global-background-color); + border: 1px solid fi-var(global-border-color); + border-radius: 0.5rem; + color: fi-var(global-font-color); + font-size: 0.875rem; + box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.15); + translate: 0 1rem; + opacity: 0; + transition: + translate 0.3s, + opacity 0.3s; + @include z-index(fixed); + + &.visible { + translate: 0 0; + opacity: 1; + } + + @starting-style { + &.visible { + translate: 0 1rem; + opacity: 0; + } + } + + @include media('print') { + display: none !important; + } +} + +.sw-update-content { + flex: 1; + min-width: 0; +} + +.sw-update-title { + margin: 0; + font-weight: 600; +} + +.sw-update-text { + margin: 0.125rem 0 0; + opacity: 0.8; +} + +.sw-update-btn { + flex-shrink: 0; + padding: 0.375rem 0.75rem; + border: none; + background-color: fi-var(primary); + color: #fff; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: opacity 0.2s; + @include border-radius; + + &:hover { + opacity: 0.85; + } +} diff --git a/hugo.toml b/hugo.toml index 55462eba..b55c8183 100644 --- a/hugo.toml +++ b/hugo.toml @@ -376,13 +376,20 @@ isPlainText = true isHTML = false permalinkable = true +# FixIt 1.0.0 | NEW Options to make output site.webmanifest file +[outputFormats.manifest] +baseName = "site" +mediaType = "application/manifest+json" +isPlainText = true +isHTML = false + # ------------------------------------------------------------------------------------- # Output Configuration # See: https://gohugo.io/configuration/outputs/ # ------------------------------------------------------------------------------------- # options to make hugo output files, the optional values are below: -# home = ["html", "rss", "archives", "offline", "link", "search", "readme", "baidu_urls"] +# home = ["html", "rss", "archives", "search", "offline", "manifest", "link", "readme", "baidu_urls"] # page = ["html", "markdown"] # section = ["html", "rss"] # taxonomy = ["html"] @@ -393,9 +400,10 @@ home = [ "html", "rss", "archives", + "search", "offline", - "link", - "search" + "manifest", + "link" ] page = [ "html", @@ -443,8 +451,6 @@ fingerprint = "" dateFormat = "2006-01-02" # website images for Open Graph and Twitter Cards images = [] -# FixIt 0.2.12 | NEW enable PWA -enablePWA = false # FixIt 0.3.13 | NEW whether to capitalize titles capitalizeTitles = true # FixIt 0.3.0 | NEW whether to add site title to the title of every page @@ -486,24 +492,45 @@ dir = "content" # available template params: {title} {URL} {sourceURL} issueTpl = "title=[BUG]%20{title}&body=|Field|Value|%0A|-|-|%0A|Title|{title}|%0A|URL|{URL}|%0A|Filename|{sourceURL}|" -# App icon config +# FixIt 1.0.0 | CHANGED App and PWA config [params.app] -# optional site title override for the app when added to an iOS home screen or Android launcher -title = "FixIt" +# whether to enable PWA support +pwa = false +# app name used for home screen and manifest (falls back to site title) +name = "" +# optional short name for the manifest (falls back to name) +short_name = "" # whether to omit favicon resource links -noFavicon = false +no_favicon = false # modern SVG favicon to use in place of older style .png and .ico files -svgFavicon = "" +svg_favicon = "" # Safari mask icon color -iconColor = "#5bbad5" +mask_color = "#5bbad5" # Windows v8-10 tile color -tileColor = "#da532c" +tile_color = "#da532c" -# FixIt 0.2.12 | CHANGED Android browser theme color -[params.app.themeColor] +# Android browser theme color +[params.app.theme_color] light = "#f6f8fa" dark = "#151b23" +# Web app manifest icons (users should provide their own icon files) +[[params.app.icons]] +src = "/apple-touch-icon.png" +sizes = "180x180" +type = "image/png" +purpose = "any maskable" + +[[params.app.icons]] +src = "/android-chrome-192x192.png" +sizes = "192x192" +type = "image/png" + +[[params.app.icons]] +src = "/android-chrome-512x512.png" +sizes = "512x512" +type = "image/png" + # Search config [params.search] enable = true diff --git a/i18n/de.toml b/i18n/de.toml index b2277681..41a90809 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -99,6 +99,7 @@ cancel = "Abbrechen" navigate = "Navigieren" select = "Auswählen" close = "Schließen" +refresh = "Aktualisieren" noResultsFound = "Keine Ergebnisse gefunden" copyToClipboard = "In Zwischenablage kopieren" copyText = "Kopieren" @@ -186,6 +187,12 @@ other = "Offline" other = "Sie sind nicht mit dem Internet verbunden, es stehen nur zwischengespeicherte Seiten zur Verfügung." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Update verfügbar" +updateText = "Eine neue Version dieser Website ist verfügbar." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Hinweis zur Weiterleitung" diff --git a/i18n/en.toml b/i18n/en.toml index 4e1a5c44..c0b7a3fe 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -99,6 +99,7 @@ cancel = "Cancel" navigate = "Navigate" select = "Select" close = "Close" +refresh = "Refresh" noResultsFound = "No results found" copyToClipboard = "Copy to clipboard" copyText = "Copy" @@ -186,6 +187,12 @@ other = "Offline" other = "You are not connected to the Internet, only cached pages will be available." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Update Available" +updateText = "A new version of this site is available." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Redirection Notice" diff --git a/i18n/es.toml b/i18n/es.toml index e2b9b0e2..25ba2565 100644 --- a/i18n/es.toml +++ b/i18n/es.toml @@ -99,6 +99,7 @@ cancel = "Cancelar" navigate = "Navegar" select = "Seleccionar" close = "Cerrar" +refresh = "Actualizar" noResultsFound = "No se encontraron resultados" copyToClipboard = "Copiar al portapapeles" copyText = "Copiar" @@ -186,6 +187,12 @@ other = "desconectado" other = "No está conectado a Internet, solo estarán disponibles las páginas almacenadas en caché." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Actualización disponible" +updateText = "Hay una nueva versión de este sitio disponible." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Aviso de redirección" diff --git a/i18n/fr.toml b/i18n/fr.toml index 1bedf87f..3e052f59 100644 --- a/i18n/fr.toml +++ b/i18n/fr.toml @@ -99,6 +99,7 @@ cancel = "Annuler" navigate = "Naviguer" select = "Sélectionner" close = "Fermer" +refresh = "Actualiser" noResultsFound = "Aucun résultat trouvé" copyToClipboard = "Copier dans le presse-papiers" copyText = "Copier" @@ -186,6 +187,12 @@ other = "Hors ligne" other = "Vous n'êtes pas connecté à Internet, seules les pages mises en cache seront disponibles." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Mise à jour disponible" +updateText = "Une nouvelle version de ce site est disponible." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Avis de redirection" diff --git a/i18n/hi.toml b/i18n/hi.toml index db694138..7ce581bf 100644 --- a/i18n/hi.toml +++ b/i18n/hi.toml @@ -100,6 +100,7 @@ cancel = "रद्द करें" navigate = "नेविगेट" select = "चुनें" close = "बंद करें" +refresh = "रिफ़्रेश" noResultsFound = "कोई परिणाम नहीं मिला" copyToClipboard = "क्लिपबोर्ड पर कॉपी करें" copyText = "कॉपी करें" @@ -187,6 +188,12 @@ other = "ऑफलाइन" other = "आप इंटरनेट से कनेक्ट नहीं हैं, केवल कैश्ड पेज ही उपलब्ध होंगे।" # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "अपडेट उपलब्ध" +updateText = "इस साइट का नया संस्करण उपलब्ध है।" +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "रीडायरेक्शन सूचना" diff --git a/i18n/it.toml b/i18n/it.toml index d5e08ce2..6a59e8a7 100644 --- a/i18n/it.toml +++ b/i18n/it.toml @@ -99,6 +99,7 @@ cancel = "Annulla" navigate = "Naviga" select = "Seleziona" close = "Chiudi" +refresh = "Aggiorna" noResultsFound = "Nessun risultato trovato" copyToClipboard = "Copia negli appunti" copyText = "Copia" @@ -186,6 +187,12 @@ other = "disconnesso" other = "Non sei connesso a Internet, saranno disponibili solo le pagine memorizzate nella cache." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Aggiornamento disponibile" +updateText = "È disponibile una nuova versione di questo sito." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Avviso di reindirizzamento" diff --git a/i18n/ja.toml b/i18n/ja.toml index 3f52bc87..1b76c483 100644 --- a/i18n/ja.toml +++ b/i18n/ja.toml @@ -97,6 +97,7 @@ cancel = "キャンセル" navigate = "移動" select = "選択" close = "閉じる" +refresh = "更新" noResultsFound = "結果が見つかりません" copyToClipboard = "クリップボードにコピー" copyText = "コピー" @@ -182,6 +183,12 @@ other = "オフライン" other = "インターネットに接続されていません。キャッシュされたページのみが利用可能です。" # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "アップデートがあります" +updateText = "このサイトの新しいバージョンが利用可能です。" +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "リダイレクト通知" diff --git a/i18n/ko.toml b/i18n/ko.toml index aaea491d..8b144b96 100644 --- a/i18n/ko.toml +++ b/i18n/ko.toml @@ -97,6 +97,7 @@ cancel = "취소" navigate = "탐색" select = "선택" close = "닫기" +refresh = "새로고침" noResultsFound = "결과를 찾을 수 없습니다" copyToClipboard = "클립보드에 복사" copyText = "복사" @@ -182,6 +183,12 @@ other = "오프라인" other = "인터넷에 연결되어 있지 않으며, 캐시된 페이지만 사용할 수 있습니다." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "업데이트 가능" +updateText = "이 사이트의 새 버전을 사용할 수 있습니다." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "리디렉션 안내" diff --git a/i18n/pl.toml b/i18n/pl.toml index 06166942..e915e64e 100644 --- a/i18n/pl.toml +++ b/i18n/pl.toml @@ -99,6 +99,7 @@ cancel = "Anuluj" navigate = "Nawiguj" select = "Wybierz" close = "Zamknij" +refresh = "Odśwież" noResultsFound = "Nie znaleziono wyników" copyToClipboard = "Skopiuj do schowka" copyText = "Kopiuj" @@ -186,6 +187,12 @@ other = "Offline" other = "Nie masz połączenia z Internetem, dostępne będą tylko strony z pamięci podręcznej." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Dostępna aktualizacja" +updateText = "Dostępna jest nowa wersja tej strony." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Powiadomienie o przekierowaniu" diff --git a/i18n/pt-BR.toml b/i18n/pt-BR.toml index 4a8a3a5c..37fafb94 100644 --- a/i18n/pt-BR.toml +++ b/i18n/pt-BR.toml @@ -100,6 +100,7 @@ cancel = "Cancelar" navigate = "Navegar" select = "Selecionar" close = "Fechar" +refresh = "Atualizar" noResultsFound = "Nenhum resultado encontrado" copyToClipboard = "Copiar para a área de transferência" copyText = "Copiar" @@ -187,6 +188,12 @@ other = "Offline" other = "Você não está conectado à Internet, apenas as páginas em cache estarão disponíveis." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Atualização disponível" +updateText = "Uma nova versão deste site está disponível." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Aviso de redirecionamento" diff --git a/i18n/ro.toml b/i18n/ro.toml index 569d0527..758c7903 100644 --- a/i18n/ro.toml +++ b/i18n/ro.toml @@ -99,6 +99,7 @@ cancel = "Anulare" navigate = "Navigare" select = "Selectare" close = "Închide" +refresh = "Reîmprospătare" noResultsFound = "Nici un rezultat gasit" copyToClipboard = "Copiați în clipboard" copyText = "Copiază" @@ -186,6 +187,12 @@ other = "Deconectat" other = "Nu sunteți conectat la Internet, vor fi disponibile doar paginile stocate în cache." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Actualizare disponibilă" +updateText = "O nouă versiune a acestui site este disponibilă." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Notificare de redirecționare" diff --git a/i18n/ru.toml b/i18n/ru.toml index 85eb7fca..2501e21e 100644 --- a/i18n/ru.toml +++ b/i18n/ru.toml @@ -99,6 +99,7 @@ cancel = "Отменить" navigate = "Навигация" select = "Выбрать" close = "Закрыть" +refresh = "Обновить" noResultsFound = "Результаты не найдены" copyToClipboard = "Копировать в буфер обмена" copyText = "Копировать" @@ -186,6 +187,12 @@ other = "Не в сети" other = "Вы не подключены к интернету, будут доступны только кешированные страницы." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Доступно обновление" +updateText = "Доступна новая версия этого сайта." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Уведомление о перенаправлении" diff --git a/i18n/sr.toml b/i18n/sr.toml index d199b6a6..403ee97b 100644 --- a/i18n/sr.toml +++ b/i18n/sr.toml @@ -99,6 +99,7 @@ cancel = "Поништи" navigate = "Навигација" select = "Изабери" close = "Затвори" +refresh = "Освежи" noResultsFound = "Резултати нису пронађени" copyToClipboard = "Копирај на радну таблу" copyText = "Копирај" @@ -186,6 +187,12 @@ other = "Оффлине" other = "Нисте повезани на Интернет, биће доступне само кеширане странице." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Ажурирање доступно" +updateText = "Нова верзија овог сајта је доступна." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Обавештење о преусмеравању" diff --git a/i18n/vi.toml b/i18n/vi.toml index 4a103445..c107597d 100644 --- a/i18n/vi.toml +++ b/i18n/vi.toml @@ -98,6 +98,7 @@ cancel = "Huỷ" navigate = "Điều hướng" select = "Chọn" close = "Đóng" +refresh = "Làm mới" noResultsFound = "Không tìm thấy kết quả" copyToClipboard = "Sao chép vào bộ nhớ tạm" copyText = "Sao chép" @@ -185,6 +186,12 @@ other = "ngoại tuyến" other = "Bạn chưa kết nối với Internet, chỉ các trang được lưu trong bộ nhớ cache sẽ khả dụng." # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "Có bản cập nhật" +updateText = "Một phiên bản mới của trang web này đã sẵn sàng." +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "Thông báo chuyển hướng" diff --git a/i18n/zh-CN.toml b/i18n/zh-CN.toml index 8f99ba39..1ff12b65 100644 --- a/i18n/zh-CN.toml +++ b/i18n/zh-CN.toml @@ -97,6 +97,7 @@ cancel = "取消" navigate = "切换" select = "选择" close = "关闭" +refresh = "刷新" noResultsFound = "没有找到结果" copyToClipboard = "复制到剪贴板" copyText = "复制" @@ -182,6 +183,12 @@ other = "离线" other = "你没有连接到 Internet,只有缓存的页面可用。" # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "发现新版本" +updateText = "当前站点有新版本可用。" +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "跳转提示" diff --git a/i18n/zh-TW.toml b/i18n/zh-TW.toml index 8a75e2f2..a21b7027 100644 --- a/i18n/zh-TW.toml +++ b/i18n/zh-TW.toml @@ -97,6 +97,7 @@ cancel = "取消" navigate = "切換" select = "選擇" close = "關閉" +refresh = "重新整理" noResultsFound = "沒有找到結果" copyToClipboard = "複製到剪貼板" copyText = "複製" @@ -182,6 +183,12 @@ other = "離線" other = "你沒有連接到 Internet,只有緩存的頁面可用。" # === Offline === +# === Service Worker === +[serviceWorker] +updateTitle = "發現新版本" +updateText = "當前站點有新版本可用。" +# === Service Worker === + # === Link Redirection === [linkRedirection] title = "跳轉提示" diff --git a/layouts/_partials/base/assets.html b/layouts/_partials/base/assets.html index 20c6f63d..8e804bea 100644 --- a/layouts/_partials/base/assets.html +++ b/layouts/_partials/base/assets.html @@ -288,17 +288,17 @@ {{- end -}} {{- /* PWA */ -}} -{{- if not hugo.IsServer | and .Site.Params.enablePWA | and hugo.IsProduction -}} +{{- if not hugo.IsServer | and hugo.IsProduction | and .Site.Params.app.pwa -}} {{- $offlineURL := "" -}} {{- with .Site.Home.OutputFormats.Get "offline" -}} {{- $offlineURL = .RelPermalink -}} {{- end -}} {{- $mainCSS := .Site.Store.Get "mainCSS" -}} {{- $mainJS := .Site.Store.Get "mainJS" -}} - {{- $swBuildCtx := dict "mainCSSURL" $mainCSS.RelPermalink "mainJSURL" $mainJS.RelPermalink "offlineURL" $offlineURL "relURL" (relURL "") "Page" . -}} + {{- $swBuildCtx := dict "mainCSSURL" $mainCSS.RelPermalink "mainJSURL" $mainJS.RelPermalink "offlineURL" $offlineURL "relURL" (relURL "") "version" (hugo.Store.Get "version") "Page" . -}} {{- $serviceWorker := resources.Get "js/service-worker.template.js" | resources.ExecuteAsTemplate "js/service-worker.template.js" $swBuildCtx -}} {{- $serviceWorker = dict "Resource" $serviceWorker "Build" (dict "targetPath" "sw.js") "Fingerprint" $fingerprint | partial "function/js-build.html" -}} - {{- $config = dict "PWA" (dict "enable" .Site.Params.enablePWA "serviceWorkerURL" $serviceWorker.RelPermalink) | merge $config -}} + {{- $config = dict "PWA" (dict "enable" .Site.Params.app.pwa "serviceWorkerURL" $serviceWorker.RelPermalink) | merge $config -}} {{- end -}} {{- /* Auto Bookmark */ -}} diff --git a/layouts/_partials/base/head/index.html b/layouts/_partials/base/head/index.html index 11019486..79c5645f 100644 --- a/layouts/_partials/base/head/index.html +++ b/layouts/_partials/base/head/index.html @@ -38,18 +38,14 @@ {{- partial "plugin/pagefind-metadata.html" . -}} - - + + -{{- with .Site.Params.app.themeColor -}} - {{- $color := . -}} - {{- if ne (len $color) 2 -}} - {{- $color = dict "light" . "dark" . -}} - {{- end -}} - +{{- with .Site.Params.app.theme_color -}} + {{- end -}} -{{- with .Site.Params.app.tileColor -}} +{{- with .Site.Params.app.tile_color -}} {{- end -}} @@ -76,9 +72,9 @@ {{- end -}} {{- /* Favicon links (static resources, generated via https://realfavicongenerator.net/) */ -}} -{{- if not .Site.Params.app.noFavicon -}} +{{- if not .Site.Params.app.no_favicon -}} {{- $relURL := relURL "" -}} - {{- with .Site.Params.app.svgFavicon -}} + {{- with .Site.Params.app.svg_favicon -}} {{- else -}} @@ -86,11 +82,11 @@ {{- end -}} - {{- with .Site.Params.app.iconColor -}} + {{- with .Site.Params.app.mask_color -}} {{- end -}} - {{- if eq .Site.Params.enablePWA true -}} - + {{- with .OutputFormats.Get "manifest" -}} + {{- if eq $.Site.Params.app.pwa true -}}{{- end -}} {{- end -}} {{- end -}} diff --git a/layouts/_partials/base/widgets.html b/layouts/_partials/base/widgets.html index c18c7156..1067ad1d 100644 --- a/layouts/_partials/base/widgets.html +++ b/layouts/_partials/base/widgets.html @@ -135,6 +135,17 @@ {{- /* Custom widgets */ -}} {{- block "custom-widgets" . }}{{ end -}} + {{- /* Service Worker Update Notification */ -}} + {{- if .Site.Params.app.pwa -}} +
+
+

{{ T "serviceWorker.updateTitle" }}

+

{{ T "serviceWorker.updateText" }}

+
+ +
+ {{- end -}} + diff --git a/layouts/home.manifest.webmanifest b/layouts/home.manifest.webmanifest new file mode 100644 index 00000000..b0b1c678 --- /dev/null +++ b/layouts/home.manifest.webmanifest @@ -0,0 +1,30 @@ +{{- if .Site.Params.app.pwa -}} + {{- $name := .Site.Params.app.name | default .Site.Title -}} + {{- $shortName := .Site.Params.app.short_name | default $name -}} + {{- $startURL := relURL "/" -}} + {{- $display := "standalone" -}} + {{- $themeColor := "" -}} + {{- with .Site.Params.app.theme_color -}} + {{- $themeColor = .light -}} + {{- end -}} + {{- $bgColor := .Site.Params.appearance.global_background_color | default "#ffffff" -}} + + {{- /* Build icons array from params.app.icons, with fallback defaults */ -}} + {{- $icons := slice -}} + {{- with .Site.Params.app.icons -}} + {{- range . -}} + {{- $icon := dict "src" .src "sizes" .sizes "type" .type -}} + {{- with .purpose -}} + {{- $icon = $icon | merge (dict "purpose" .) -}} + {{- end -}} + {{- $icons = $icons | append $icon -}} + {{- end -}} + {{- else -}} + {{- $icons = $icons | append (dict "src" (printf "%sapple-touch-icon.png" (relURL "")) "sizes" "180x180" "type" "image/png" "purpose" "any maskable") -}} + {{- $icons = $icons | append (dict "src" (printf "%sandroid-chrome-192x192.png" (relURL "")) "sizes" "192x192" "type" "image/png") -}} + {{- $icons = $icons | append (dict "src" (printf "%sandroid-chrome-512x512.png" (relURL "")) "sizes" "512x512" "type" "image/png") -}} + {{- end -}} + + {{- $manifest := dict "name" $name "short_name" $shortName "start_url" $startURL "display" $display "theme_color" $themeColor "background_color" $bgColor "icons" $icons -}} + {{- $manifest | jsonify (dict "indent" " ") -}} +{{- end -}}