mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 15:28:57 +00:00
fix(assets): rewrite service worker with two-strategy caching (#774)
- Replace legacy service-worker.js (imported from DoIt) with a clean rewrite using Hugo's ExecuteAsTemplate for fingerprinted URL injection - Cache-first for static assets (immutable/fingerprinted), network-first for HTML pages with LRU eviction (max 100 entries) - Fix LRU eviction to protect precached URLs using absolute URL comparison - Add Fingerprint param to js-build.html partial to reduce boilerplate - Extract to-css.html partial for reusable SCSS-to-CSS pipeline - Support pre-built Resource param in script.html and style.html - Store mainCSS and mainJS in Site.Store to avoid duplicate builds - Remove explicit scope from serviceWorker.register() (defaults to /) Closes #298
This commit is contained in:
Vendored
+2
@@ -9,6 +9,8 @@
|
||||
"vercel.json": "build.sh"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.template.ts": "gots",
|
||||
"*.template.js": "gojs",
|
||||
"*.template.scss": "gocss",
|
||||
"*.template.svg": "html",
|
||||
"**/plugin/script.html": "gohtml",
|
||||
|
||||
@@ -53,7 +53,7 @@ export class MiscModule implements MiscService {
|
||||
initServiceWorker() {
|
||||
if (this.core.config.PWA?.enable && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register(this.core.config.PWA.serviceWorkerURL, { scope: '/' })
|
||||
.register(this.core.config.PWA.serviceWorkerURL)
|
||||
.then((_registration) => {
|
||||
// console.log('Service Worker Registered');
|
||||
})
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
/**
|
||||
* Service Worker
|
||||
* imported from https://github.com/HEIGE-PCloud/DoIt/blob/v0.2.11/src/js/sw.js
|
||||
* [todo] rewrite with TypeScript and fixes issue #298
|
||||
*/
|
||||
const CACHE_VERSION = 1;
|
||||
|
||||
const BASE_CACHE_FILES = [
|
||||
'/css/style.min.css',
|
||||
'/js/theme.min.js',
|
||||
'/site.webmanifest',
|
||||
];
|
||||
|
||||
const OFFLINE_CACHE_FILES = [
|
||||
'/css/style.min.css',
|
||||
'/js/theme.min.js',
|
||||
'/site.webmanifest',
|
||||
'/offline/'
|
||||
];
|
||||
|
||||
const NOT_FOUND_CACHE_FILES = [
|
||||
'/css/style.min.css',
|
||||
'/js/theme.min.js',
|
||||
'/site.webmanifest',
|
||||
'/404.html'
|
||||
];
|
||||
|
||||
const OFFLINE_PAGE = '/offline/';
|
||||
const NOT_FOUND_PAGE = '/404.html';
|
||||
|
||||
const CACHE_VERSIONS = {
|
||||
assets: 'assets-v' + CACHE_VERSION,
|
||||
content: 'content-v' + CACHE_VERSION,
|
||||
offline: 'offline-v' + CACHE_VERSION,
|
||||
notFound: '404-v' + CACHE_VERSION
|
||||
};
|
||||
|
||||
// Define MAX_TTL's in SECONDS for specific file extensions
|
||||
const MAX_TTL = {
|
||||
'/': 3600,
|
||||
html: 3600,
|
||||
json: 86400,
|
||||
js: 86400,
|
||||
css: 86400
|
||||
};
|
||||
|
||||
const CACHE_BLACKLIST = [
|
||||
(str) => {
|
||||
return !str.startsWith('http://localhost');
|
||||
}
|
||||
];
|
||||
|
||||
const SUPPORTED_METHODS = ['GET'];
|
||||
|
||||
/**
|
||||
* isBlackListed
|
||||
* @param {string} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isBlacklisted(url) {
|
||||
return CACHE_BLACKLIST.length > 0
|
||||
? !CACHE_BLACKLIST.filter((rule) => {
|
||||
if (typeof rule === 'function') {
|
||||
return !rule(url);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}).length
|
||||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* getFileExtension
|
||||
* @param {string} url
|
||||
* @returns {string}
|
||||
*/
|
||||
function getFileExtension(url) {
|
||||
const extension = url.split('.').reverse()[0].split('?')[0];
|
||||
return extension.endsWith('/') ? '/' : extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* getTTL
|
||||
* @param {string} url
|
||||
*/
|
||||
function getTTL(url) {
|
||||
if (typeof url === 'string') {
|
||||
const extension = getFileExtension(url);
|
||||
if (typeof MAX_TTL[extension] === 'number') {
|
||||
return MAX_TTL[extension];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* installServiceWorker
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function installServiceWorker() {
|
||||
return Promise.all([
|
||||
caches.open(CACHE_VERSIONS.assets).then((cache) => {
|
||||
return cache.addAll(BASE_CACHE_FILES);
|
||||
}),
|
||||
caches.open(CACHE_VERSIONS.offline).then((cache) => {
|
||||
return cache.addAll(OFFLINE_CACHE_FILES);
|
||||
}),
|
||||
caches.open(CACHE_VERSIONS.notFound).then((cache) => {
|
||||
return cache.addAll(NOT_FOUND_CACHE_FILES);
|
||||
})
|
||||
]).then(() => {
|
||||
return self.skipWaiting();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* cleanupLegacyCache
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function cleanupLegacyCache() {
|
||||
const currentCaches = Object.keys(CACHE_VERSIONS).map((key) => {
|
||||
return CACHE_VERSIONS[key];
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) => {
|
||||
return keys.filter((key) => {
|
||||
return !~currentCaches.indexOf(key);
|
||||
});
|
||||
})
|
||||
.then((legacy) => {
|
||||
if (legacy.length) {
|
||||
Promise.all(
|
||||
legacy.map((legacyKey) => {
|
||||
return caches.delete(legacyKey);
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(Promise.all([installServiceWorker(), self.skipWaiting()]));
|
||||
});
|
||||
|
||||
// The activate handler takes care of cleaning up old caches.
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
cleanupLegacyCache(),
|
||||
self.clients.claim(),
|
||||
self.skipWaiting()
|
||||
]).catch((err) => {
|
||||
console.warn(err);
|
||||
self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.open(CACHE_VERSIONS.content).then((cache) => {
|
||||
return cache
|
||||
.match(event.request)
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
const headers = response.headers.entries();
|
||||
let date = null;
|
||||
|
||||
for (const pair of headers) {
|
||||
if (pair[0] === 'date') {
|
||||
date = new Date(pair[1]);
|
||||
}
|
||||
}
|
||||
if (date) {
|
||||
const age = parseInt(
|
||||
(new Date().getTime() - date.getTime()) / 1000
|
||||
);
|
||||
const ttl = getTTL(event.request.url);
|
||||
|
||||
if (ttl && age > ttl) {
|
||||
return new Promise((resolve) => {
|
||||
return fetch(event.request.clone())
|
||||
.then((updatedResponse) => {
|
||||
if (updatedResponse) {
|
||||
cache.put(event.request, updatedResponse.clone());
|
||||
resolve(updatedResponse);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(response);
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn(err);
|
||||
return response;
|
||||
});
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
return response;
|
||||
} else {
|
||||
return fetch(event.request.clone())
|
||||
.then((response) => {
|
||||
if (response.status < 400) {
|
||||
if (
|
||||
~SUPPORTED_METHODS.indexOf(event.request.method) &&
|
||||
!isBlacklisted(event.request.url) &&
|
||||
event.request.url.slice(0, 4) === 'http'
|
||||
) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} else {
|
||||
return caches.open(CACHE_VERSIONS.notFound).then((cache) => {
|
||||
return cache.match(NOT_FOUND_PAGE);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return caches
|
||||
.open(CACHE_VERSIONS.offline)
|
||||
.then((offlineCache) => {
|
||||
return offlineCache.match(OFFLINE_PAGE);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(' Error in fetch handler:', error);
|
||||
throw error;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/* eslint-disable no-restricted-globals -- `self` is the standard Service Worker global */
|
||||
|
||||
/**
|
||||
* Service Worker
|
||||
* @description Two-strategy caching with the native Cache API:
|
||||
* - Static assets (fingerprinted CSS/JS): cache-first, immutable
|
||||
* - 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.
|
||||
* @see https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook
|
||||
*/
|
||||
|
||||
/* ========== Constants ========== */
|
||||
|
||||
const CACHE_NAME = 'fixit-v1'
|
||||
|
||||
/** URLs to pre-cache during the install event (injected by Hugo). */
|
||||
const PRECACHE_URLS = [
|
||||
'{{ .mainCSSURL }}',
|
||||
'{{ .mainJSURL }}',
|
||||
'{{ .relURL }}site.webmanifest',
|
||||
'{{ .offlineURL }}',
|
||||
'{{ .relURL }}404.html',
|
||||
]
|
||||
|
||||
const OFFLINE_PAGE = '{{ .offlineURL }}'
|
||||
const NOT_FOUND_PAGE = '{{ .relURL }}404.html'
|
||||
|
||||
/** Maximum number of HTML pages to keep in cache (LRU eviction). */
|
||||
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']
|
||||
|
||||
/* ========== Helpers ========== */
|
||||
|
||||
/** Cached cache instance — avoids repeated caches.open() calls. */
|
||||
const cachePromise = caches.open(CACHE_NAME)
|
||||
|
||||
/**
|
||||
* Check if a URL is a same-origin static asset (fingerprinted by Hugo).
|
||||
* Returns false for HTML pages and extensionless paths.
|
||||
*/
|
||||
function isStaticAsset(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 STATIC_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()).
|
||||
* Skips pre-cached URLs (offline/404 pages) to ensure they are never evicted.
|
||||
*/
|
||||
async function evictOldEntries() {
|
||||
const cache = await cachePromise
|
||||
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 excess = htmlKeys.length - MAX_HTML_CACHE_ENTRIES
|
||||
if (excess > 0) {
|
||||
await Promise.all(htmlKeys.slice(0, excess).map(req => cache.delete(req)))
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Lifecycle ========== */
|
||||
|
||||
/** Pre-cache core assets on install (individual calls for resilience). */
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
cachePromise
|
||||
.then(cache => Promise.allSettled(
|
||||
PRECACHE_URLS.filter(Boolean).map(url => cache.add(url)),
|
||||
))
|
||||
.then(() => self.skipWaiting()),
|
||||
)
|
||||
})
|
||||
|
||||
/** Clean up old caches, evict stale HTML entries, and take control immediately. */
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)),
|
||||
))
|
||||
.then(() => evictOldEntries())
|
||||
.then(() => self.clients.claim()),
|
||||
)
|
||||
})
|
||||
|
||||
/* ========== Fetch Strategies ========== */
|
||||
|
||||
/** Route same-origin GET requests to the appropriate caching strategy. */
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event
|
||||
|
||||
// Cache API only supports GET
|
||||
if (request.method !== 'GET' || !request.url.startsWith(self.location.origin))
|
||||
return
|
||||
|
||||
event.respondWith(
|
||||
isStaticAsset(request.url)
|
||||
? cacheFirst(request)
|
||||
: networkFirst(request),
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Cache-first strategy for fingerprinted static assets.
|
||||
* These are immutable — a content change produces a new URL,
|
||||
* so cached responses are always valid.
|
||||
*/
|
||||
async function cacheFirst(request) {
|
||||
const cache = await cachePromise
|
||||
const cached = await cache.match(request)
|
||||
if (cached)
|
||||
return cached
|
||||
|
||||
try {
|
||||
const response = await fetch(request)
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone())
|
||||
}
|
||||
return response
|
||||
}
|
||||
catch {
|
||||
return new Response('Offline', { status: 503 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Network-first strategy for HTML pages.
|
||||
* Always tries the network for fresh content, falls back to cache on failure.
|
||||
*/
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const response = await fetch(request)
|
||||
if (response.ok) {
|
||||
const cache = await cachePromise
|
||||
cache.put(request, response.clone())
|
||||
// Evict oldest HTML entries to keep cache bounded
|
||||
evictOldEntries()
|
||||
}
|
||||
// Server error (4xx/5xx) — try 404 page from cache
|
||||
if (response.status >= 400) {
|
||||
return (await caches.match(NOT_FOUND_PAGE)) || response
|
||||
}
|
||||
return response
|
||||
}
|
||||
catch {
|
||||
// Network failure — try cached page, then offline fallback
|
||||
try {
|
||||
return (await caches.match(request)) || (await caches.match(OFFLINE_PAGE)) || new Response('Offline', { status: 503 })
|
||||
}
|
||||
catch {
|
||||
return new Response('Offline', { status: 503 })
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -9,7 +9,6 @@ export default antfu({
|
||||
'layouts/**/*.json',
|
||||
'layouts/**/*.xml',
|
||||
'layouts/**/*.md',
|
||||
// ignore temporarily
|
||||
'assets/js/service-worker.js',
|
||||
'**/*.template.*',
|
||||
],
|
||||
})
|
||||
|
||||
@@ -290,12 +290,16 @@
|
||||
|
||||
{{- /* PWA */ -}}
|
||||
{{- if not hugo.IsServer | and .Site.Params.enablePWA | and hugo.IsProduction -}}
|
||||
{{- $serviceWorker := dict "Resource" (resources.Get "js/service-worker.js") "Build" true | partial "function/js-build.html" -}}
|
||||
{{- with $fingerprint -}}
|
||||
{{- $serviceWorker = $serviceWorker | fingerprint . -}}
|
||||
{{- $offlineURL := "" -}}
|
||||
{{- with .Site.Home.OutputFormats.Get "offline" -}}
|
||||
{{- $offlineURL = .RelPermalink -}}
|
||||
{{- end -}}
|
||||
{{- $serviceWorkerURL := $serviceWorker.RelPermalink -}}
|
||||
{{- $config = dict "PWA" (dict "enable" .Site.Params.enablePWA "serviceWorkerURL" $serviceWorkerURL) | merge $config -}}
|
||||
{{- $mainCSS := .Site.Store.Get "mainCSS" -}}
|
||||
{{- $mainJS := .Site.Store.Get "mainJS" -}}
|
||||
{{- $swBuildCtx := dict "mainCSSURL" $mainCSS.RelPermalink "mainJSURL" $mainJS.RelPermalink "offlineURL" $offlineURL "relURL" (relURL "") "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 -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Auto Bookmark */ -}}
|
||||
@@ -366,10 +370,7 @@
|
||||
{{- $layoutLoaders := $mermaid.layoutloaders | default slice -}}
|
||||
|
||||
{{- $options := dict "format" "esm" -}}
|
||||
{{- $mermaidModule := dict "Resource" (resources.Get "js/lib/mermaid.ts") "Build" $options | partial "function/js-build.html" -}}
|
||||
{{- with $fingerprint -}}
|
||||
{{- $mermaidModule = $mermaidModule | fingerprint . -}}
|
||||
{{- end -}}
|
||||
{{- $mermaidModule := dict "Resource" (resources.Get "js/lib/mermaid.ts") "Build" $options "Fingerprint" $fingerprint | partial "function/js-build.html" -}}
|
||||
{{- $mermaidModuleURL := $mermaidModule.RelPermalink -}}
|
||||
|
||||
{{- $bootstrapJS := dict
|
||||
@@ -384,8 +385,8 @@
|
||||
{{- dict "Content" $bootstrapJS "Path" "js/lib/mermaid-bootstrap.js" "Minify" hugo.IsProduction "Attr" `type="module"` "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Theme main bundle */ -}}
|
||||
{{- dict "Source" "js/main.ts" "Build" true "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
|
||||
{{- /* Theme main bundle (built once in head/index.html) */ -}}
|
||||
{{- dict "Resource" (.Site.Store.Get "mainJS") "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}}
|
||||
|
||||
{{- /* Custom assets block */ -}}
|
||||
{{- block "custom-assets" . }}{{ end -}}
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
{{- $title = .Site.Title -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Favicon links */ -}}
|
||||
{{/* [todo] 先判断有图片资源时才加载,没有时提示引导 https://realfavicongenerator.net/ */}}
|
||||
{{- /* Favicon links (static resources, generated via https://realfavicongenerator.net/) */ -}}
|
||||
{{- if not .Site.Params.app.noFavicon -}}
|
||||
{{- $relURL := relURL "" -}}
|
||||
{{- with .Site.Params.app.svgFavicon -}}
|
||||
@@ -125,12 +124,14 @@
|
||||
-}}
|
||||
|
||||
{{- /* Theme main CSS */ -}}
|
||||
{{- dict
|
||||
"Source" "scss/main.scss"
|
||||
{{- $mainCSS := dict
|
||||
"Resource" (resources.Get "scss/main.scss")
|
||||
"ToCSS" true
|
||||
"Fingerprint" $fingerprint
|
||||
| partial "plugin/style.html"
|
||||
| partial "function/to-css.html"
|
||||
-}}
|
||||
{{- .Site.Store.Set "mainCSS" $mainCSS -}}
|
||||
{{- dict "Resource" $mainCSS | partial "plugin/style.html" -}}
|
||||
|
||||
{{- /* Font Awesome Icons */ -}}
|
||||
{{- $source := $cdn.fontawesomeFreeCSS | default "lib/fontawesome-free/all.min.css" -}}
|
||||
@@ -143,10 +144,8 @@
|
||||
{{- partial "plugin/style.html" $options -}}
|
||||
|
||||
{{- /* Preload main theme bundle */ -}}
|
||||
{{- $mainResource := dict "Resource" (resources.Get "js/main.ts") "Build" true | partial "function/js-build.html" -}}
|
||||
{{- with $fingerprint -}}
|
||||
{{- $mainResource = $mainResource | fingerprint . -}}
|
||||
{{- end -}}
|
||||
{{- $mainResource := dict "Resource" (resources.Get "js/main.ts") "Build" true "Fingerprint" $fingerprint | partial "function/js-build.html" -}}
|
||||
{{- .Site.Store.Set "mainJS" $mainResource -}}
|
||||
<link rel="preload" href="{{ $mainResource.RelPermalink }}" as="script">
|
||||
|
||||
{{- /* ========== SEO: Structured Data (Schema.org) ========== */ -}}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@param {resource.Resource} .Resource Hugo resource to process
|
||||
@param {Object|Boolean} [.Build] true for defaults, dict for custom options, false/omit to skip js.Build
|
||||
@param {Boolean} [.Minify] extra minify flag; final minify is Minify OR Build.minify
|
||||
@param {String} [.Fingerprint] fingerprint algorithm (e.g. "sha256")
|
||||
@return {resource.Resource}
|
||||
*/ -}}
|
||||
|
||||
@@ -25,4 +26,7 @@
|
||||
{{- if $minify -}}
|
||||
{{- $resource = $resource | minify -}}
|
||||
{{- end -}}
|
||||
{{- with .Fingerprint -}}
|
||||
{{- $resource = $resource | fingerprint . -}}
|
||||
{{- end -}}
|
||||
{{- return $resource -}}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{{- /*
|
||||
Compile SCSS to CSS via dartsass, optionally minify and fingerprint.
|
||||
@param {resource.Resource} .Resource SCSS resource
|
||||
@param {Object|Boolean} [.ToCSS] true for defaults, dict for custom toCSS options, false/omit to skip
|
||||
@param {Boolean} [.Minify] whether to minify; defaults to hugo.IsProduction when ToCSS is used
|
||||
@param {String} [.Fingerprint] fingerprint algorithm (e.g. "sha256")
|
||||
@return {resource.Resource}
|
||||
*/ -}}
|
||||
|
||||
{{- $resource := .Resource -}}
|
||||
{{- $minify := .Minify -}}
|
||||
{{- with .ToCSS -}}
|
||||
{{- $targetPath := replaceRE `\.(template\.)?scss$` ".css" $resource.Name -}}
|
||||
{{- $targetPath = replace $targetPath "scss" "css" -}}
|
||||
{{- $defaultOptions := dict
|
||||
"transpiler" "dartsass"
|
||||
"enableSourceMap" hugo.IsDevelopment
|
||||
"outputStyle" (cond hugo.IsDevelopment "expanded" "compressed")
|
||||
"targetPath" $targetPath
|
||||
-}}
|
||||
{{- $options := . -}}
|
||||
{{- if eq $options true -}}
|
||||
{{- $options = dict -}}
|
||||
{{- end -}}
|
||||
{{- $options = $options | merge $defaultOptions -}}
|
||||
{{- $resource = $resource | toCSS $options -}}
|
||||
{{- $minify = $minify | default hugo.IsProduction -}}
|
||||
{{- end -}}
|
||||
{{- if $minify -}}
|
||||
{{- $resource = $resource | minify -}}
|
||||
{{- end -}}
|
||||
{{- with .Fingerprint -}}
|
||||
{{- $resource = $resource | fingerprint . -}}
|
||||
{{- end -}}
|
||||
{{- return $resource -}}
|
||||
@@ -1,8 +1,9 @@
|
||||
{{- /*
|
||||
Script tag renderer.
|
||||
- Accepts direct script HTML or builds script source from local/remote resources.
|
||||
- Accepts direct script HTML, a pre-built resource, or builds from source path.
|
||||
- Supports template execution, minify/build/fingerprint pipeline, and extra attrs.
|
||||
@param {String|Resource} .Source script source URL/path or Hugo resource; if string starts with "<script" it's treated as raw HTML
|
||||
@param {String} [.Source] script source URL/path; if starts with "<script" it's treated as raw HTML
|
||||
@param {resource.Resource} [.Resource] pre-built resource (skips build pipeline)
|
||||
@param {String} [.Content] inline content to write as generated resource
|
||||
@param {String} [.Path] target path for resource created from Content
|
||||
@param {Object} [.Template] target path for resource created from template execution
|
||||
@@ -17,21 +18,31 @@
|
||||
@param {String} [.Attr] additional attributes
|
||||
*/ -}}
|
||||
|
||||
{{- if strings.HasPrefix .Source "<script" -}}
|
||||
{{- if strings.HasPrefix (.Source | default "") "<script" -}}
|
||||
{{- safeHTML .Source -}}
|
||||
{{- else -}}
|
||||
{{- /* Script source */ -}}
|
||||
{{- $src := .Source -}}
|
||||
{{- $src := "" -}}
|
||||
{{- $integrity := .Integrity -}}
|
||||
{{- $minify := .Minify -}}
|
||||
{{- if $src | and (not (urls.Parse $src).Host) -}}
|
||||
|
||||
{{- /* Pre-built resource */ -}}
|
||||
{{- with .Resource -}}
|
||||
{{- $src = .RelPermalink -}}
|
||||
{{- with .Data.Integrity -}}
|
||||
{{- $integrity = . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Source build */ -}}
|
||||
{{- if not $src -}}
|
||||
{{- $src = .Source -}}
|
||||
{{- end -}}
|
||||
{{- if $src | and (not (urls.Parse $src).Host) | and (not .Resource) -}}
|
||||
{{- $resource := resources.Get $src -}}
|
||||
{{- with .Template -}}
|
||||
{{- $resource = resources.ExecuteAsTemplate . $.Context $resource -}}
|
||||
{{- end -}}
|
||||
{{- $resource = dict "Resource" $resource "Build" .Build "Minify" $minify | partial "function/js-build.html" -}}
|
||||
{{- $resource = dict "Resource" $resource "Build" .Build "Minify" .Minify "Fingerprint" .Fingerprint | partial "function/js-build.html" -}}
|
||||
{{- with .Fingerprint -}}
|
||||
{{- $resource = $resource | fingerprint . -}}
|
||||
{{- $integrity = $resource.Data.Integrity -}}
|
||||
{{- end -}}
|
||||
{{- with $resource.RelPermalink -}}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{{- /*
|
||||
Stylesheet tag renderer.
|
||||
- Accepts direct <link> HTML or builds CSS resources from local paths/content.
|
||||
- Accepts direct <link> HTML, a pre-built resource, or builds from source path.
|
||||
- Supports template execution, optional toCSS, minify/fingerprint, and preload mode.
|
||||
@param {String} .Source stylesheet URL/path or Hugo resource; if string starts with "<link" it's treated as raw HTML
|
||||
@param {String} [.Source] stylesheet URL/path; if starts with "<link" it's treated as raw HTML
|
||||
@param {resource.Resource} [.Resource] pre-built resource (skips build pipeline)
|
||||
@param {String} [.Content] inline style content to write as generated resource
|
||||
@param {String} [.Path] target path for resource created from Content
|
||||
@param {Object} [.Template] target path for resource created from template execution
|
||||
@@ -16,50 +17,46 @@
|
||||
@param {String} [.Attr] additional attributes
|
||||
*/ -}}
|
||||
|
||||
{{- if strings.HasPrefix .Source "<link" -}}
|
||||
{{- if strings.HasPrefix (.Source | default "") "<link" -}}
|
||||
{{- safeHTML .Source -}}
|
||||
{{- else -}}
|
||||
{{- $href := .Source -}}
|
||||
{{- $href := "" -}}
|
||||
{{- $integrity := .Integrity -}}
|
||||
{{- $resource := 0 -}}
|
||||
{{- if $href | and (not (urls.Parse $href).Host) -}}
|
||||
{{- $resource = resources.Get $href -}}
|
||||
|
||||
{{- /* Pre-built resource */ -}}
|
||||
{{- with .Resource -}}
|
||||
{{- $href = .RelPermalink -}}
|
||||
{{- with .Data.Integrity -}}
|
||||
{{- $integrity = . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- with .Content -}}
|
||||
{{- $resource = resources.FromString $.Path . -}}
|
||||
|
||||
{{- /* Source build */ -}}
|
||||
{{- if not $href -}}
|
||||
{{- $href = .Source -}}
|
||||
{{- end -}}
|
||||
{{- if $resource -}}
|
||||
{{- $minify := .Minify -}}
|
||||
{{- if $href | and (not (urls.Parse $href).Host) | and (not .Resource) -}}
|
||||
{{- $resource := resources.Get $href -}}
|
||||
{{- with .Template -}}
|
||||
{{- $resource = resources.ExecuteAsTemplate . $.Context $resource -}}
|
||||
{{- end -}}
|
||||
{{- with .ToCSS -}}
|
||||
{{- $targetPath := replaceRE `\.(template\.)?scss$` ".css" $resource.Name -}}
|
||||
{{- $targetPath = replace $targetPath "scss" "css" -}}
|
||||
{{- $defaultOptions := dict
|
||||
"transpiler" "dartsass"
|
||||
"enableSourceMap" hugo.IsDevelopment
|
||||
"outputStyle" (cond hugo.IsDevelopment "expanded" "compressed")
|
||||
"targetPath" $targetPath
|
||||
"silenceDeprecations" (slice "import" "global-builtin" "color-functions" "function-units")
|
||||
-}}
|
||||
{{- $options := . -}}
|
||||
{{- if eq . true -}}
|
||||
{{- $options = dict -}}
|
||||
{{- end -}}
|
||||
{{- $options = $options | merge $defaultOptions -}}
|
||||
{{- $resource = $resource | toCSS $options -}}
|
||||
{{- $minify = $minify | default hugo.IsProduction -}}
|
||||
{{- end -}}
|
||||
{{- if $minify -}}
|
||||
{{- $resource = $resource | minify -}}
|
||||
{{- end -}}
|
||||
{{- $resource = dict "Resource" $resource "ToCSS" .ToCSS "Minify" .Minify "Fingerprint" .Fingerprint | partial "function/to-css.html" -}}
|
||||
{{- with .Fingerprint -}}
|
||||
{{- $resource = $resource | fingerprint . -}}
|
||||
{{- $integrity = $resource.Data.Integrity -}}
|
||||
{{- end -}}
|
||||
{{- $href = $resource.RelPermalink -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Style from string */ -}}
|
||||
{{- if .Content | and .Path -}}
|
||||
{{- $contentResource := resources.FromString .Path .Content -}}
|
||||
{{- if .Minify -}}
|
||||
{{- $contentResource = $contentResource | minify -}}
|
||||
{{- end -}}
|
||||
{{- $href = $contentResource.RelPermalink -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Style attributes */ -}}
|
||||
{{- $attrs := printf `href="%v"` $href -}}
|
||||
{{- if .Crossorigin -}}
|
||||
{{- $attrs = ` crossorigin="anonymous"` | add $attrs -}}
|
||||
|
||||
Reference in New Issue
Block a user