Skip to content

Commit 639dbac

Browse files
committed
Added support for auto-replacement of meta tags when using instant loading
1 parent 15538b0 commit 639dbac

File tree

16 files changed

+156
-91
lines changed

16 files changed

+156
-91
lines changed

material/overrides/assets/javascripts/custom.4eda089e.min.js renamed to material/overrides/assets/javascripts/custom.a4bbca43.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/overrides/assets/javascripts/custom.4eda089e.min.js.map renamed to material/overrides/assets/javascripts/custom.a4bbca43.min.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/overrides/main.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@
2323
{% endblock %}
2424
{% block scripts %}
2525
{{ super() }}
26-
<script src="{{ 'assets/javascripts/custom.4eda089e.min.js' | url }}"></script>
26+
<script src="{{ 'assets/javascripts/custom.a4bbca43.min.js' | url }}"></script>
2727
{% endblock %}

material/templates/assets/javascripts/bundle.5827baa9.min.js

Lines changed: 0 additions & 29 deletions
This file was deleted.

material/templates/assets/javascripts/bundle.5827baa9.min.js.map

Lines changed: 0 additions & 8 deletions
This file was deleted.

material/templates/assets/javascripts/bundle.726fbb30.min.js

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/templates/assets/javascripts/bundle.726fbb30.min.js.map

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

material/templates/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@
250250
</script>
251251
{% endblock %}
252252
{% block scripts %}
253-
<script src="{{ 'assets/javascripts/bundle.5827baa9.min.js' | url }}"></script>
253+
<script src="{{ 'assets/javascripts/bundle.726fbb30.min.js' | url }}"></script>
254254
{% for script in config.extra_javascript %}
255255
{{ script | script_tag }}
256256
{% endfor %}

src/templates/assets/javascripts/_/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export type Flag =
3838
| "header.autohide" /* Hide header */
3939
| "navigation.expand" /* Automatic expansion */
4040
| "navigation.indexes" /* Section pages */
41-
| "navigation.instant" /* Instant loading */
41+
| "navigation.instant" /* Instant navigation */
4242
| "navigation.sections" /* Section navigation */
4343
| "navigation.tabs" /* Tabs navigation */
4444
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */

src/templates/assets/javascripts/browser/location/_/index.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222

2323
import { Subject } from "rxjs"
2424

25+
import { feature } from "~/_"
26+
import { h } from "~/utilities"
27+
2528
/* ----------------------------------------------------------------------------
2629
* Functions
2730
* ------------------------------------------------------------------------- */
@@ -43,10 +46,31 @@ export function getLocation(): URL {
4346
/**
4447
* Set location
4548
*
46-
* @param url - URL to change to
49+
* If instant navigation is enabled, this function creates a temporary anchor
50+
* element, sets the `href` attribute, appends it to the body, clicks it, and
51+
* then removes it again. The event will bubble up the DOM and trigger be
52+
* intercepted by the instant loading business logic.
53+
*
54+
* Note that we must append and remove the anchor element, or the event will
55+
* not bubble up the DOM, making it impossible to intercept it.
56+
*
57+
* @param url - URL to navigate to
58+
* @param navigate - Force navigation
4759
*/
48-
export function setLocation(url: URL | HTMLLinkElement): void {
49-
location.href = url.href
60+
export function setLocation(
61+
url: URL | HTMLLinkElement, navigate = false
62+
): void {
63+
if (feature("navigation.instant") && !navigate) {
64+
const el = h("a", { href: url.href })
65+
document.body.appendChild(el)
66+
el.click()
67+
el.remove()
68+
69+
// If we're not using instant navigation, and the page should not be reloaded
70+
// just instruct the browser to navigate to the given URL
71+
} else {
72+
location.href = url.href
73+
}
5074
}
5175

5276
/* ------------------------------------------------------------------------- */

src/templates/assets/javascripts/bundle.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ import {
7777
import {
7878
SearchIndex,
7979
setupClipboardJS,
80-
setupInstantLoading,
80+
setupInstantNavigation,
8181
setupVersionSelector
8282
} from "./integrations"
8383
import {
@@ -143,9 +143,9 @@ const index$ = document.forms.namedItem("search")
143143
const alert$ = new Subject<string>()
144144
setupClipboardJS({ alert$ })
145145

146-
/* Set up instant loading, if enabled */
146+
/* Set up instant navigation, if enabled */
147147
if (feature("navigation.instant"))
148-
setupInstantLoading({ location$, viewport$ })
148+
setupInstantNavigation({ location$, viewport$ })
149149
.subscribe(document$)
150150

151151
/* Set up version selector */
@@ -175,21 +175,15 @@ keyboard$
175175
case ",":
176176
const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]")
177177
if (typeof prev !== "undefined")
178-
if (feature("navigation.instant"))
179-
location$.next(new URL(prev.href))
180-
else
181-
setLocation(prev)
178+
setLocation(prev)
182179
break
183180

184181
/* Go to next page */
185182
case "n":
186183
case ".":
187184
const next = getOptionalElement<HTMLLinkElement>("link[rel=next]")
188185
if (typeof next !== "undefined")
189-
if (feature("navigation.instant"))
190-
location$.next(new URL(next.href))
191-
else
192-
setLocation(next)
186+
setLocation(next)
193187
break
194188

195189
/* Expand navigation, see https://bit.ly/3ZjG5io */

src/templates/assets/javascripts/components/announce/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function mountAnnounce(
8282
if (!feature("announce.dismiss") || !el.childElementCount)
8383
return EMPTY
8484

85-
/* Support instant loading - see https://t.ly/3FTme */
85+
/* Support instant navigation - see https://t.ly/3FTme */
8686
if (!el.hidden) {
8787
const content = getElement(".md-typeset", el)
8888
if (__md_hash(content.innerHTML) === __md_get("__announce"))

src/templates/assets/javascripts/components/search/query/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function watchSearchQuery(
117117
first(active => !active)
118118
)
119119
.subscribe(() => {
120-
const url = new URL(location.href)
120+
const url = getLocation()
121121
url.searchParams.delete("q")
122122
history.replaceState({}, "", `${url}`)
123123
})

src/templates/assets/javascripts/integrations/instant/index.ts

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,48 @@ interface SetupOptions {
7070
viewport$: Observable<Viewport> /* Viewport observable */
7171
}
7272

73+
/* ----------------------------------------------------------------------------
74+
* Helper functions
75+
* ------------------------------------------------------------------------- */
76+
77+
/**
78+
* Create a map of head elements for lookup and replacement
79+
*
80+
* @param head - Document head
81+
*
82+
* @returns Element map
83+
*/
84+
function lookup(head: HTMLHeadElement): Map<string, HTMLElement> {
85+
const tags = new Map<string, HTMLElement>()
86+
for (const el of getElements(":scope > *", head)) {
87+
let html = el.outerHTML
88+
89+
// If the current element is a style sheet or script, we must resolve the
90+
// URL relative to the current location and make it absolute, so it's easy
91+
// to deduplicate it later on by comparing the outer HTML of tags. We must
92+
// keep identical style sheets and scripts without replacing them.
93+
for (const key of ["href", "src"]) {
94+
const value = el.getAttribute(key)!
95+
if (value === null)
96+
continue
97+
98+
// Resolve URL relative to current location
99+
const url = new URL(value, getLocation())
100+
const ref = el.cloneNode() as HTMLElement
101+
102+
// Set resolved URL and retrieve HTML for deduplication
103+
ref.setAttribute(key, `${url}`)
104+
html = ref.outerHTML
105+
}
106+
107+
// Index element in tag map
108+
tags.set(html, el)
109+
}
110+
111+
// Return tag map
112+
return tags
113+
}
114+
73115
/* ----------------------------------------------------------------------------
74116
* Functions
75117
* ------------------------------------------------------------------------- */
@@ -84,7 +126,7 @@ interface SetupOptions {
84126
*
85127
* @returns Document observable
86128
*/
87-
export function setupInstantLoading(
129+
export function setupInstantNavigation(
88130
{ location$, viewport$ }: SetupOptions
89131
): Observable<Document> {
90132
const config = configuration()
@@ -188,10 +230,10 @@ export function setupInstantLoading(
188230
history.pushState(null, "", url)
189231
})
190232

191-
// Emit URL that should be fetched via instant loading on location subject,
192-
// which was passed into this function. The idea is that instant loading can
193-
// be intercepted by other parts of the application, which can synchronously
194-
// back up or restore state before instant loading happens.
233+
// Emit URL that should be fetched via instant navigation on location subject,
234+
// which was passed into this function. Instant navigation can be intercepted
235+
// by other parts of the application, which can synchronously back up or
236+
// restore state before instant navigation happens.
195237
instant$.subscribe(location$)
196238

197239
// Fetch document - when fetching, we could use `responseType: document`, but
@@ -201,7 +243,7 @@ export function setupInstantLoading(
201243
// reason, we fall back to regular navigation and set the location explicitly,
202244
// which will force-load the page. Furthermore, we must pre-warm the buffer
203245
// for the duplicate check, or the first click on an anchor link will also
204-
// trigger an instant loading event, which doesn't make sense.
246+
// trigger an instant navigation event, which doesn't make sense.
205247
const response$ = location$
206248
.pipe(
207249
startWith(getLocation()),
@@ -210,32 +252,22 @@ export function setupInstantLoading(
210252
switchMap(url => request(url)
211253
.pipe(
212254
catchError(() => {
213-
setLocation(url)
255+
setLocation(url, true)
214256
return EMPTY
215257
})
216258
)
217259
)
218260
)
219261

220262
// Initialize the DOM parser, parse the returned HTML, and replace selected
221-
// meta tags and components before handing control down to the application
263+
// components before handing control down to the application
222264
const dom = new DOMParser()
223265
const document$ = response$
224266
.pipe(
225267
switchMap(res => res.text()),
226268
switchMap(res => {
227-
const document = dom.parseFromString(res, "text/html")
269+
const next = dom.parseFromString(res, "text/html")
228270
for (const selector of [
229-
230-
// Meta tags
231-
"title",
232-
"link[rel=prev]",
233-
"link[rel=next]",
234-
"link[rel=canonical]",
235-
"meta[name=author]",
236-
"meta[name=description]",
237-
238-
// Components
239271
"[data-md-component=announce]",
240272
"[data-md-component=container]",
241273
"[data-md-component=header-topic]",
@@ -247,7 +279,7 @@ export function setupInstantLoading(
247279
: []
248280
]) {
249281
const source = getOptionalElement(selector)
250-
const target = getOptionalElement(selector, document)
282+
const target = getOptionalElement(selector, next)
251283
if (
252284
typeof source !== "undefined" &&
253285
typeof target !== "undefined"
@@ -256,13 +288,28 @@ export function setupInstantLoading(
256288
}
257289
}
258290

259-
// After meta tags and components were replaced, re-evaluate scripts
291+
// Update meta tags
292+
const source = lookup(document.head)
293+
const target = lookup(next.head)
294+
for (const [html, el] of target) {
295+
if (source.has(html)) {
296+
source.delete(html)
297+
} else {
298+
document.head.appendChild(el)
299+
}
300+
}
301+
302+
// Remove meta tags that are not present in the new document
303+
for (const el of source.values())
304+
el.remove()
305+
306+
// After components and meta tags were replaced, re-evaluate scripts
260307
// that were provided by the author as part of Markdown files
261308
const container = getComponentElement("container")
262309
return concat(getElements("script", container))
263310
.pipe(
264311
switchMap(el => {
265-
const script = document.createElement("script")
312+
const script = next.createElement("script")
266313
if (el.src) {
267314
for (const name of el.getAttributeNames())
268315
script.setAttribute(name, el.getAttribute(name)!)
@@ -281,7 +328,7 @@ export function setupInstantLoading(
281328
}
282329
}),
283330
ignoreElements(),
284-
endWith(document)
331+
endWith(next)
285332
)
286333
}),
287334
share()

src/templates/assets/javascripts/integrations/sitemap/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export type Sitemap = string[]
5050
* Preprocess a list of URLs
5151
*
5252
* This function replaces the `site_url` in the sitemap with the actual base
53-
* URL, to allow instant loading to work in occasions like Netlify previews.
53+
* URL, to allow instant navigation to work in occasions like Netlify previews.
5454
*
5555
* @param urls - URLs
5656
*

src/templates/assets/javascripts/integrations/version/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ export function setupVersionSelector(
112112
// find the same page, as we might have different deployments
113113
// due to aliases. However, if we're outside the version
114114
// selector, we must abort here, because we might otherwise
115-
// interfere with instant loading. We need to refactor this
116-
// at some point together with instant loading.
115+
// interfere with instant navigation. We need to refactor this
116+
// at some point together with instant navigation.
117117
//
118118
// See https://github.com/squidfunk/mkdocs-material/issues/4012
119119
if (!ev.target.closest(".md-version")) {
@@ -143,7 +143,7 @@ export function setupVersionSelector(
143143
)
144144
)
145145
)
146-
.subscribe(url => setLocation(url))
146+
.subscribe(url => setLocation(url, true))
147147

148148
/* Render version selector and warning */
149149
combineLatest([versions$, current$])
@@ -152,7 +152,7 @@ export function setupVersionSelector(
152152
topic.appendChild(renderVersionSelector(versions, current))
153153
})
154154

155-
/* Integrate outdated version banner with instant loading */
155+
/* Integrate outdated version banner with instant navigation */
156156
document$.pipe(switchMap(() => current$))
157157
.subscribe(current => {
158158

0 commit comments

Comments
 (0)