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
This commit is contained in:
Cell
2026-06-24 12:43:37 +08:00
committed by GitHub
parent 47247e01c5
commit 5cad4d3df9
31 changed files with 450 additions and 91 deletions
+1
View File
@@ -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. */
+3
View File
@@ -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)
+6 -1
View File
@@ -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
+1
View File
@@ -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()
}
-20
View File
@@ -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()
+72
View File
@@ -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<HTMLElement>('.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'))
})
}
}
+63 -16
View File
@@ -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())
@@ -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 <body> 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: {{</* admonition ban */>}} 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);
}
+1
View File
@@ -8,3 +8,4 @@
@use "noscript-warning";
@use "reading-progress";
@use "scrollbar";
@use "sw-toast";
+72
View File
@@ -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;
}
}
+41 -14
View File
@@ -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
+7
View File
@@ -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"
+7
View File
@@ -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"
+7
View File
@@ -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"
+7
View File
@@ -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"
+7
View File
@@ -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 = "रीडायरेक्शन सूचना"
+7
View File
@@ -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"
+7
View File
@@ -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 = "リダイレクト通知"
+7
View File
@@ -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 = "리디렉션 안내"
+7
View File
@@ -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"
+7
View File
@@ -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"
+7
View File
@@ -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"
+7
View File
@@ -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 = "Уведомление о перенаправлении"
+7
View File
@@ -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 = "Обавештење о преусмеравању"
+7
View File
@@ -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"
+7
View File
@@ -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 = "跳转提示"
+7
View File
@@ -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 = "跳轉提示"
+3 -3
View File
@@ -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 */ -}}
+10 -14
View File
@@ -38,18 +38,14 @@
{{- partial "plugin/pagefind-metadata.html" . -}}
<meta name="application-name" content="{{ .Site.Params.app.title | default .Site.Title }}">
<meta name="apple-mobile-web-app-title" content="{{ .Site.Params.app.title | default .Site.Title }}">
<meta name="application-name" content="{{ .Site.Params.app.name | default .Site.Title }}">
<meta name="apple-mobile-web-app-title" content="{{ .Site.Params.app.name | default .Site.Title }}">
{{- with .Site.Params.app.themeColor -}}
{{- $color := . -}}
{{- if ne (len $color) 2 -}}
{{- $color = dict "light" . "dark" . -}}
{{- end -}}
<meta name="theme-color" data-light="{{ $color.light }}" data-dark="{{ $color.dark }}" content="{{ $color.light }}">
{{- with .Site.Params.app.theme_color -}}
<meta name="theme-color" data-light="{{ .light }}" data-dark="{{ .dark }}" content="{{ .light }}">
{{- end -}}
{{- with .Site.Params.app.tileColor -}}
{{- with .Site.Params.app.tile_color -}}
<meta name="msapplication-TileColor" content="{{ . }}">
{{- 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 -}}
<link rel="icon" href="{{ . }}">
{{- else -}}
<link rel="shortcut icon" type="image/x-icon" href="{{ $relURL }}favicon.ico" />
@@ -86,11 +82,11 @@
<link rel="icon" type="image/png" sizes="16x16" href="{{ $relURL }}favicon-16x16.png">
{{- end -}}
<link rel="apple-touch-icon" sizes="180x180" href="{{ $relURL }}apple-touch-icon.png">
{{- with .Site.Params.app.iconColor -}}
{{- with .Site.Params.app.mask_color -}}
<link rel="mask-icon" href="{{ $relURL }}safari-pinned-tab.svg" color="{{ . }}">
{{- end -}}
{{- if eq .Site.Params.enablePWA true -}}
<link rel="manifest" href="{{ $relURL }}site.webmanifest">
{{- with .OutputFormats.Get "manifest" -}}
{{- if eq $.Site.Params.app.pwa true -}}<link rel="manifest" href="{{ .RelPermalink }}">{{- end -}}
{{- end -}}
{{- end -}}
+11
View File
@@ -135,6 +135,17 @@
{{- /* Custom widgets */ -}}
{{- block "custom-widgets" . }}{{ end -}}
{{- /* Service Worker Update Notification */ -}}
{{- if .Site.Params.app.pwa -}}
<div class="sw-update-notification">
<div class="sw-update-content">
<p class="sw-update-title">{{ T "serviceWorker.updateTitle" }}</p>
<p class="sw-update-text">{{ T "serviceWorker.updateText" }}</p>
</div>
<button type="button" class="sw-update-btn">{{ T "assets.refresh" }}</button>
</div>
{{- end -}}
<noscript>
<div class="noscript-warning">{{ T "baseof.noscript" }}</div>
</noscript>
+30
View File
@@ -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 -}}