@daz4126/swifty 2.12.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/assets.js +30 -0
- package/src/client/IDIOMORPH-LICENSE.txt +13 -0
- package/src/client/idiomorph.esm.js +1385 -0
- package/src/client/swifty-navigation.js +399 -0
- package/src/config.js +14 -0
- package/src/init.js +3 -0
- package/src/layout.js +13 -8
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { Idiomorph } from "./idiomorph.esm.js";
|
|
2
|
+
|
|
3
|
+
if (typeof window !== "undefined") window.Idiomorph ||= Idiomorph;
|
|
4
|
+
|
|
5
|
+
class NavigationFallback extends Error {
|
|
6
|
+
constructor(url, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.url = url;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class SwiftyNavigation {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.targetSelector = options.target || "main";
|
|
15
|
+
this.prefetching = options.prefetching !== false;
|
|
16
|
+
this.cacheSize = Number(options.cacheSize || 20);
|
|
17
|
+
this.cacheTTL = Number(options.cacheTTL || 15) * 1000;
|
|
18
|
+
this.cache = new Map();
|
|
19
|
+
this.sequence = 0;
|
|
20
|
+
this.controller = null;
|
|
21
|
+
this.prefetchTimer = null;
|
|
22
|
+
this.boundClick = (event) => this.handleClick(event);
|
|
23
|
+
this.boundPopState = (event) => this.handlePopState(event);
|
|
24
|
+
this.boundPrefetchIntent = (event) => this.handlePrefetchIntent(event);
|
|
25
|
+
this.boundInvalidate = () => this.clearCache();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start() {
|
|
29
|
+
if (this.started) return;
|
|
30
|
+
this.started = true;
|
|
31
|
+
|
|
32
|
+
history.scrollRestoration = "manual";
|
|
33
|
+
this.rememberScroll();
|
|
34
|
+
document.addEventListener("click", this.boundClick);
|
|
35
|
+
window.addEventListener("popstate", this.boundPopState);
|
|
36
|
+
document.addEventListener("mouseover", this.boundPrefetchIntent);
|
|
37
|
+
document.addEventListener("focusin", this.boundPrefetchIntent);
|
|
38
|
+
document.addEventListener("touchstart", this.boundPrefetchIntent, { passive: true });
|
|
39
|
+
document.addEventListener("swifty:invalidate", this.boundInvalidate);
|
|
40
|
+
requestAnimationFrame(() => {
|
|
41
|
+
this.dispatch("swifty:load", {
|
|
42
|
+
url: window.location.href,
|
|
43
|
+
navigationType: "initial",
|
|
44
|
+
prefetched: false,
|
|
45
|
+
target: document.querySelector(this.targetSelector),
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
stop() {
|
|
51
|
+
if (!this.started) return;
|
|
52
|
+
|
|
53
|
+
document.removeEventListener("click", this.boundClick);
|
|
54
|
+
window.removeEventListener("popstate", this.boundPopState);
|
|
55
|
+
document.removeEventListener("mouseover", this.boundPrefetchIntent);
|
|
56
|
+
document.removeEventListener("focusin", this.boundPrefetchIntent);
|
|
57
|
+
document.removeEventListener("touchstart", this.boundPrefetchIntent);
|
|
58
|
+
document.removeEventListener("swifty:invalidate", this.boundInvalidate);
|
|
59
|
+
clearTimeout(this.prefetchTimer);
|
|
60
|
+
this.controller?.abort();
|
|
61
|
+
this.started = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleClick(event) {
|
|
65
|
+
const link = this.eligibleLink(event.target);
|
|
66
|
+
if (!link || !this.eligibleClick(event, link)) return;
|
|
67
|
+
|
|
68
|
+
const url = new URL(link.href, window.location.href);
|
|
69
|
+
if (this.onlyChangesHash(url)) return;
|
|
70
|
+
|
|
71
|
+
const detail = { url: url.href, link, navigationType: "link" };
|
|
72
|
+
if (!this.dispatch("swifty:before-navigate", detail, true)) return;
|
|
73
|
+
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
this.rememberScroll();
|
|
76
|
+
this.navigate(url.href, { historyMode: "push", navigationType: "link" });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handlePopState(event) {
|
|
80
|
+
const detail = { url: window.location.href, navigationType: "popstate" };
|
|
81
|
+
if (!this.dispatch("swifty:before-navigate", detail, true)) {
|
|
82
|
+
window.location.reload();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const scroll = event.state?.swifty?.scroll;
|
|
87
|
+
this.navigate(window.location.href, {
|
|
88
|
+
historyMode: "pop",
|
|
89
|
+
navigationType: "popstate",
|
|
90
|
+
scroll,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
handlePrefetchIntent(event) {
|
|
95
|
+
if (!this.prefetchEnabled()) return;
|
|
96
|
+
|
|
97
|
+
const link = this.eligibleLink(event.target);
|
|
98
|
+
if (!link || !this.eligibleURL(new URL(link.href, window.location.href))) return;
|
|
99
|
+
if (link.closest('[data-swifty-prefetch="off"]')) return;
|
|
100
|
+
if (event.type === "mouseover" && link.contains(event.relatedTarget)) return;
|
|
101
|
+
|
|
102
|
+
clearTimeout(this.prefetchTimer);
|
|
103
|
+
const delay = event.type === "mouseover" ? 75 : 0;
|
|
104
|
+
this.prefetchTimer = setTimeout(() => this.prefetch(link.href), delay);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async navigate(url, options = {}) {
|
|
108
|
+
const sequence = ++this.sequence;
|
|
109
|
+
this.controller?.abort();
|
|
110
|
+
this.controller = null;
|
|
111
|
+
this.setLoading(true, { url, navigationType: options.navigationType });
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
let prefetched = false;
|
|
115
|
+
let page = await this.takePrefetch(url);
|
|
116
|
+
if (page?.cacheable) {
|
|
117
|
+
prefetched = true;
|
|
118
|
+
} else {
|
|
119
|
+
this.controller = new AbortController();
|
|
120
|
+
page = await this.fetchPage(url, { signal: this.controller.signal });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sequence !== this.sequence) return;
|
|
124
|
+
|
|
125
|
+
const beforeMorph = {
|
|
126
|
+
url: page.url,
|
|
127
|
+
navigationType: options.navigationType,
|
|
128
|
+
prefetched,
|
|
129
|
+
newTarget: page.target,
|
|
130
|
+
};
|
|
131
|
+
if (!this.dispatch("swifty:before-morph", beforeMorph, true)) {
|
|
132
|
+
throw new NavigationFallback(page.url, "morph was cancelled");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.morph(page);
|
|
136
|
+
this.updateHistory(page.url, options.historyMode);
|
|
137
|
+
await this.nextFrame();
|
|
138
|
+
if (sequence !== this.sequence) return;
|
|
139
|
+
this.restoreScroll(page.url, options);
|
|
140
|
+
this.manageFocus(page.url, options.navigationType);
|
|
141
|
+
|
|
142
|
+
this.dispatch("swifty:load", {
|
|
143
|
+
url: page.url,
|
|
144
|
+
navigationType: options.navigationType,
|
|
145
|
+
prefetched,
|
|
146
|
+
target: document.querySelector(this.targetSelector),
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.name === "AbortError" || sequence !== this.sequence) return;
|
|
150
|
+
|
|
151
|
+
const fallbackURL = error instanceof NavigationFallback ? error.url : url;
|
|
152
|
+
this.dispatch("swifty:navigation-error", { url: fallbackURL, error });
|
|
153
|
+
this.fullLoad(fallbackURL, options.historyMode === "pop");
|
|
154
|
+
} finally {
|
|
155
|
+
if (sequence === this.sequence) {
|
|
156
|
+
this.setLoading(false, { url, navigationType: options.navigationType });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async fetchPage(url, { prefetch = false, signal } = {}) {
|
|
162
|
+
const response = await fetch(url, {
|
|
163
|
+
method: "GET",
|
|
164
|
+
credentials: "same-origin",
|
|
165
|
+
redirect: "follow",
|
|
166
|
+
signal,
|
|
167
|
+
headers: {
|
|
168
|
+
Accept: "text/html",
|
|
169
|
+
"X-Swifty-Navigation": "true",
|
|
170
|
+
...(prefetch ? { "X-Swifty-Prefetch": "true" } : {}),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const finalURL = response.url || url;
|
|
175
|
+
if (!response.ok) throw new NavigationFallback(finalURL, `HTTP ${response.status}`);
|
|
176
|
+
if (!this.eligibleURL(new URL(finalURL, window.location.href))) {
|
|
177
|
+
throw new NavigationFallback(finalURL, "redirected outside the current origin");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const contentType = response.headers.get("content-type") || "";
|
|
181
|
+
if (!contentType.includes("text/html")) {
|
|
182
|
+
throw new NavigationFallback(finalURL, "response is not HTML");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const source = await response.text();
|
|
186
|
+
const parsed = new DOMParser().parseFromString(source, "text/html");
|
|
187
|
+
const target = parsed.querySelector(this.targetSelector);
|
|
188
|
+
if (!target) throw new NavigationFallback(finalURL, "navigation target is missing");
|
|
189
|
+
|
|
190
|
+
const cacheControl = response.headers.get("cache-control") || "";
|
|
191
|
+
return {
|
|
192
|
+
url: finalURL,
|
|
193
|
+
target,
|
|
194
|
+
title: parsed.title,
|
|
195
|
+
cacheable: !cacheControl.includes("no-store"),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
morph(page) {
|
|
200
|
+
const current = document.querySelector(this.targetSelector);
|
|
201
|
+
if (!current) throw new NavigationFallback(page.url, "current navigation target is missing");
|
|
202
|
+
|
|
203
|
+
Idiomorph.morph(current, page.target, {
|
|
204
|
+
morphStyle: "outerHTML",
|
|
205
|
+
restoreFocus: false,
|
|
206
|
+
callbacks: {
|
|
207
|
+
beforeNodeMorphed: (oldNode) => {
|
|
208
|
+
if (oldNode.nodeType === Node.ELEMENT_NODE && oldNode.hasAttribute("data-swifty-permanent")) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (page.title) document.title = page.title;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async prefetch(url) {
|
|
219
|
+
const key = this.cacheKey(url);
|
|
220
|
+
const existing = this.cache.get(key);
|
|
221
|
+
if (existing && existing.expiresAt > Date.now()) return existing.promise;
|
|
222
|
+
if (existing) this.cache.delete(key);
|
|
223
|
+
|
|
224
|
+
const promise = this.fetchPage(key, { prefetch: true })
|
|
225
|
+
.then((page) => {
|
|
226
|
+
if (!page.cacheable) this.cache.delete(key);
|
|
227
|
+
return page;
|
|
228
|
+
})
|
|
229
|
+
.catch((error) => {
|
|
230
|
+
this.cache.delete(key);
|
|
231
|
+
if (!(error instanceof NavigationFallback)) console.debug("Swifty prefetch failed", error);
|
|
232
|
+
return null;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
this.cache.set(key, { promise, expiresAt: Date.now() + this.cacheTTL });
|
|
236
|
+
this.pruneCache();
|
|
237
|
+
return promise;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async takePrefetch(url) {
|
|
241
|
+
const key = this.cacheKey(url);
|
|
242
|
+
const entry = this.cache.get(key);
|
|
243
|
+
if (!entry) return null;
|
|
244
|
+
|
|
245
|
+
this.cache.delete(key);
|
|
246
|
+
if (entry.expiresAt <= Date.now()) return null;
|
|
247
|
+
return entry.promise;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
pruneCache() {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
for (const [key, entry] of this.cache) {
|
|
253
|
+
if (entry.expiresAt <= now) this.cache.delete(key);
|
|
254
|
+
}
|
|
255
|
+
while (this.cache.size > this.cacheSize) this.cache.delete(this.cache.keys().next().value);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
clearCache() {
|
|
259
|
+
this.cache.clear();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
eligibleClick(event, link) {
|
|
263
|
+
return event.button === 0 &&
|
|
264
|
+
!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey &&
|
|
265
|
+
!event.defaultPrevented &&
|
|
266
|
+
(!link.target || link.target === "_self");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
eligibleLink(node) {
|
|
270
|
+
const link = node instanceof Element ? node.closest("a[href]") : null;
|
|
271
|
+
if (!link || link.hasAttribute("download")) return null;
|
|
272
|
+
if (link.closest('[data-swifty-navigation="off"]')) return null;
|
|
273
|
+
if (link.relList.contains("external")) return null;
|
|
274
|
+
|
|
275
|
+
const url = new URL(link.href, window.location.href);
|
|
276
|
+
return this.eligibleURL(url) ? link : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
eligibleURL(url) {
|
|
280
|
+
return url.origin === window.location.origin && ["http:", "https:"].includes(url.protocol);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
onlyChangesHash(url) {
|
|
284
|
+
return url.pathname === window.location.pathname &&
|
|
285
|
+
url.search === window.location.search &&
|
|
286
|
+
url.hash &&
|
|
287
|
+
url.hash !== window.location.hash;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
prefetchEnabled() {
|
|
291
|
+
const connection = navigator.connection;
|
|
292
|
+
return this.prefetching &&
|
|
293
|
+
!connection?.saveData &&
|
|
294
|
+
!["slow-2g", "2g"].includes(connection?.effectiveType);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
cacheKey(url) {
|
|
298
|
+
const value = new URL(url, window.location.href);
|
|
299
|
+
value.hash = "";
|
|
300
|
+
return value.href;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
rememberScroll() {
|
|
304
|
+
const state = history.state || {};
|
|
305
|
+
history.replaceState({
|
|
306
|
+
...state,
|
|
307
|
+
swifty: {
|
|
308
|
+
...(state.swifty || {}),
|
|
309
|
+
url: window.location.href,
|
|
310
|
+
scroll: { x: window.scrollX, y: window.scrollY },
|
|
311
|
+
},
|
|
312
|
+
}, "", window.location.href);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
updateHistory(url, mode) {
|
|
316
|
+
const state = { swifty: { url, scroll: { x: 0, y: 0 } } };
|
|
317
|
+
if (mode === "push") history.pushState(state, "", url);
|
|
318
|
+
else if (mode === "pop" && url !== window.location.href) history.replaceState(state, "", url);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
restoreScroll(url, options) {
|
|
322
|
+
if (options.historyMode === "pop" && options.scroll) {
|
|
323
|
+
window.scrollTo(options.scroll.x || 0, options.scroll.y || 0);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const hash = new URL(url, window.location.href).hash;
|
|
328
|
+
const anchor = this.anchorFor(hash);
|
|
329
|
+
if (anchor) anchor.scrollIntoView();
|
|
330
|
+
else window.scrollTo(0, 0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
manageFocus(url, navigationType) {
|
|
334
|
+
if (navigationType === "popstate") return;
|
|
335
|
+
|
|
336
|
+
const hash = new URL(url, window.location.href).hash;
|
|
337
|
+
const target = this.anchorFor(hash) ||
|
|
338
|
+
document.querySelector(`${this.targetSelector} [autofocus]`) ||
|
|
339
|
+
document.querySelector(this.targetSelector);
|
|
340
|
+
if (!target) return;
|
|
341
|
+
|
|
342
|
+
const addedTabIndex = !target.hasAttribute("tabindex") && !target.matches("a,button,input,select,textarea");
|
|
343
|
+
if (addedTabIndex) target.setAttribute("tabindex", "-1");
|
|
344
|
+
target.focus({ preventScroll: true });
|
|
345
|
+
if (addedTabIndex) target.addEventListener("blur", () => target.removeAttribute("tabindex"), { once: true });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
anchorFor(hash) {
|
|
349
|
+
if (!hash) return null;
|
|
350
|
+
try {
|
|
351
|
+
return document.getElementById(decodeURIComponent(hash.slice(1)));
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
nextFrame() {
|
|
358
|
+
return new Promise((resolve) => requestAnimationFrame(resolve));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
setLoading(loading, detail) {
|
|
362
|
+
const root = document.documentElement;
|
|
363
|
+
const target = document.querySelector(this.targetSelector);
|
|
364
|
+
if (loading) {
|
|
365
|
+
root.setAttribute("data-swifty-navigating", "");
|
|
366
|
+
target?.setAttribute("aria-busy", "true");
|
|
367
|
+
this.dispatch("swifty:navigation-start", detail);
|
|
368
|
+
} else {
|
|
369
|
+
root.removeAttribute("data-swifty-navigating");
|
|
370
|
+
target?.removeAttribute("aria-busy");
|
|
371
|
+
this.dispatch("swifty:navigation-end", detail);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
dispatch(name, detail, cancelable = false) {
|
|
376
|
+
return document.dispatchEvent(new CustomEvent(name, { detail, cancelable }));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fullLoad(url, replace = false) {
|
|
380
|
+
if (replace) window.location.replace(url);
|
|
381
|
+
else window.location.assign(url);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const script = document.querySelector("script[data-swifty-navigation]");
|
|
386
|
+
if (script) {
|
|
387
|
+
const navigation = new SwiftyNavigation({
|
|
388
|
+
target: script.dataset.target,
|
|
389
|
+
prefetching: script.dataset.prefetching !== "off",
|
|
390
|
+
cacheSize: script.dataset.cacheSize,
|
|
391
|
+
cacheTTL: script.dataset.cacheTtl,
|
|
392
|
+
});
|
|
393
|
+
window.SwiftyNavigation = navigation;
|
|
394
|
+
if (document.readyState === "loading") {
|
|
395
|
+
document.addEventListener("DOMContentLoaded", () => navigation.start(), { once: true });
|
|
396
|
+
} else {
|
|
397
|
+
navigation.start();
|
|
398
|
+
}
|
|
399
|
+
}
|
package/src/config.js
CHANGED
|
@@ -48,9 +48,23 @@ const builtInDefaults = {
|
|
|
48
48
|
default_page_count: 2,
|
|
49
49
|
// Build safety
|
|
50
50
|
build_concurrency: 16,
|
|
51
|
+
// Navigation
|
|
52
|
+
morphing: true,
|
|
53
|
+
prefetching: true,
|
|
54
|
+
morph_target: 'main',
|
|
55
|
+
navigation_cache_size: 20,
|
|
56
|
+
navigation_cache_ttl: 15,
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
const loadedConfig = await loadConfig(baseDir);
|
|
60
|
+
const hasConfig = (key) => Object.prototype.hasOwnProperty.call(loadedConfig, key);
|
|
61
|
+
|
|
62
|
+
if (hasConfig('turbo')) {
|
|
63
|
+
console.warn('The "turbo" config option is deprecated. Use "morphing" and "prefetching" instead.');
|
|
64
|
+
if (!hasConfig('morphing')) {
|
|
65
|
+
loadedConfig.morphing = loadedConfig.turbo;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
54
68
|
const defaultConfig = { ...builtInDefaults, ...loadedConfig };
|
|
55
69
|
|
|
56
70
|
export { baseDir, dirs, defaultConfig, loadConfig };
|
package/src/init.js
CHANGED
package/src/layout.js
CHANGED
|
@@ -7,6 +7,15 @@ import { getCssImports, getJsImports, getCssPreloads, getJsPreloads } from "./as
|
|
|
7
7
|
const layoutCache = new Map();
|
|
8
8
|
let template = null;
|
|
9
9
|
|
|
10
|
+
const escapeAttr = (value) =>
|
|
11
|
+
String(value || "")
|
|
12
|
+
.replace(/&/g, "&")
|
|
13
|
+
.replace(/"/g, """)
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">");
|
|
16
|
+
|
|
17
|
+
const navigationEnabled = () => defaultConfig.morphing !== false;
|
|
18
|
+
|
|
10
19
|
const getLayout = async (layoutName) => {
|
|
11
20
|
if (!layoutName) return null;
|
|
12
21
|
if (!layoutCache.has(layoutName)) {
|
|
@@ -30,14 +39,10 @@ const createTemplate = async () => {
|
|
|
30
39
|
const preconnectHints = [
|
|
31
40
|
'<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>',
|
|
32
41
|
'<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">',
|
|
33
|
-
...(defaultConfig.turbo ? [
|
|
34
|
-
'<link rel="preconnect" href="https://esm.sh" crossorigin>',
|
|
35
|
-
'<link rel="dns-prefetch" href="https://esm.sh">',
|
|
36
|
-
] : []),
|
|
37
42
|
].join('\n');
|
|
38
43
|
|
|
39
|
-
const
|
|
40
|
-
? `<script type="module"
|
|
44
|
+
const navigationScript = navigationEnabled()
|
|
45
|
+
? `<script type="module" src="/swifty/swifty-navigation.js" data-swifty-navigation data-target="${escapeAttr(defaultConfig.morph_target || "main")}" data-prefetching="${defaultConfig.prefetching === false ? "off" : "intent"}" data-cache-size="${escapeAttr(defaultConfig.navigation_cache_size || 20)}" data-cache-ttl="${escapeAttr(defaultConfig.navigation_cache_ttl || 15)}"></script>`
|
|
41
46
|
: '';
|
|
42
47
|
const livereloadScript = process.env.SWIFTY_WATCH
|
|
43
48
|
? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':${defaultConfig.livereload_port || 35729}/livereload.js?snipver=1"></' + 'script>')</script>`
|
|
@@ -54,7 +59,7 @@ const createTemplate = async () => {
|
|
|
54
59
|
const highlightCSS = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/monokai-sublime.min.css">`;
|
|
55
60
|
|
|
56
61
|
// Order: preconnect hints -> preloads -> actual assets -> scripts
|
|
57
|
-
const template = templateContent.replace('</head>', `${preconnectHints}\n${preloads}\n${
|
|
62
|
+
const template = templateContent.replace('</head>', `${preconnectHints}\n${preloads}\n${navigationScript}\n${highlightCSS}\n${imports}\n${livereloadScript}\n</head>`);
|
|
58
63
|
return template;
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -77,4 +82,4 @@ const resetCaches = async () => {
|
|
|
77
82
|
template = await createTemplate();
|
|
78
83
|
};
|
|
79
84
|
|
|
80
|
-
export { getLayout, applyLayoutAndWrapContent, getTemplate, resetCaches };
|
|
85
|
+
export { getLayout, applyLayoutAndWrapContent, getTemplate, resetCaches };
|