@jxsuite/compiler 1.4.0 → 2.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/dist/cli.js +564 -230
- package/dist/compiler.js +334 -133
- package/package.json +11 -9
- package/src/compiler.ts +1 -1
- package/src/shadow.ts +65 -0
- package/src/shared.ts +461 -51
- package/src/site/bundler.ts +149 -18
- package/src/site/client-runtime.ts +374 -0
- package/src/site/context-injection.ts +45 -2
- package/src/site/csp.ts +224 -0
- package/src/site/head-merger.ts +57 -13
- package/src/site/headers-emitter.ts +232 -0
- package/src/site/i18n.ts +365 -0
- package/src/site/image-optimizer.ts +7 -0
- package/src/site/image-transform.ts +269 -118
- package/src/site/img-loading.ts +55 -0
- package/src/site/link-relations.ts +251 -0
- package/src/site/locale-negotiation.ts +258 -0
- package/src/site/pages-discovery.ts +149 -0
- package/src/site/preview-server.ts +9 -1
- package/src/site/schema-command.ts +138 -87
- package/src/site/service-worker.ts +294 -0
- package/src/site/site-build.ts +956 -109
- package/src/site/site-loader.ts +1 -0
- package/src/site/validate-command.ts +134 -38
- package/src/site/well-known.ts +221 -0
- package/src/targets/compile-client.ts +144 -81
- package/src/targets/compile-element.ts +620 -58
- package/src/targets/compile-server.ts +19 -2
- package/src/targets/compile-static.ts +12 -3
- package/src/types.ts +2 -0
package/src/site/i18n.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale routing: which language a route is in, and what that means for the page it produces.
|
|
3
|
+
*
|
|
4
|
+
* The `i18n` key has been in the project schema since the beginning and **nothing read it**. A site
|
|
5
|
+
* with `pages/fr/about.json` got a French page only because `fr` happened to be an ordinary path
|
|
6
|
+
* segment; the config beside it was decoration. This module is the reader.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately not a translation system. Jx has no message catalogue, no `t()` and no fallback
|
|
9
|
+
* chain — a locale here is a property of a _route_, and what the route serves is whatever the
|
|
10
|
+
* author put in that directory. That is the whole model, and everything below follows from it.
|
|
11
|
+
*
|
|
12
|
+
* @docs framework/site/i18n
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { canonicalizeLocale, localeDirection } from "@jxsuite/schema/locale";
|
|
16
|
+
import type { ResolvedI18n, TextDirection } from "@jxsuite/schema/locale";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* `resolveI18n` and the two types beside it live in `@jxsuite/schema/locale` rather than here.
|
|
20
|
+
* Studio needs the same answer and cannot import this package — there is no `./site/i18n` export,
|
|
21
|
+
* the dependency graph carries `sharp` and `esbuild`, and Studio bundles for a browser. One
|
|
22
|
+
* resolution with two hosts is the only shape in which the tag Studio offers is certain to be the
|
|
23
|
+
* tag the build accepts.
|
|
24
|
+
*
|
|
25
|
+
* Everything below is route-shaped and has no Studio consumer, so it stays.
|
|
26
|
+
*/
|
|
27
|
+
export { resolveI18n } from "@jxsuite/schema/locale";
|
|
28
|
+
export type { LocaleRouting, ResolvedI18n } from "@jxsuite/schema/locale";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The locale a URL belongs to.
|
|
32
|
+
*
|
|
33
|
+
* Matching is on the **first path segment**, compared canonically, so `/fr-CA/about/` resolves for
|
|
34
|
+
* a project that declared `fr-ca`. A path under no declared locale is the default locale — under
|
|
35
|
+
* `prefix-except-default` that is the point, and under `prefix-always` it is a page the author put
|
|
36
|
+
* outside the locale tree, which is theirs to place.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} urlPattern - Site-absolute route pattern, e.g. `/fr/about/`
|
|
39
|
+
* @param {ResolvedI18n | null} i18n
|
|
40
|
+
* @returns {string | null} A canonical tag, or null when the project has no i18n
|
|
41
|
+
*/
|
|
42
|
+
export function localeOfRoute(urlPattern: string, i18n: ResolvedI18n | null): string | null {
|
|
43
|
+
if (i18n === null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const first = urlPattern.split("/").find(Boolean);
|
|
47
|
+
if (first === undefined) {
|
|
48
|
+
return i18n.defaultLocale;
|
|
49
|
+
}
|
|
50
|
+
const canonical = canonicalizeLocale(first);
|
|
51
|
+
return canonical !== null && i18n.locales.includes(canonical) ? canonical : i18n.defaultLocale;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Routes sitting outside the locale tree under `prefix-always`.
|
|
56
|
+
*
|
|
57
|
+
* `prefix-always` is a promise that **every** URL names its language. A page at `/about/` under it
|
|
58
|
+
* breaks that promise silently: it builds, it serves, and `localeOfRoute` calls it the default
|
|
59
|
+
* locale — so a site that declared "no unprefixed URLs" ships them anyway, and the only reader who
|
|
60
|
+
* finds out is a visitor who lands on one and sees the wrong language with no way to switch.
|
|
61
|
+
*
|
|
62
|
+
* Reported rather than rejected. The author may genuinely mean it — a `/robots-info/` page, a
|
|
63
|
+
* landing page with no translations — and failing their build over a page that works would be the
|
|
64
|
+
* compiler overruling a decision it cannot see the reason for. Naming it is enough.
|
|
65
|
+
*
|
|
66
|
+
* `/` is excluded: under `prefix-always` the site root is the one URL that exists precisely to send
|
|
67
|
+
* a visitor somewhere else, and negotiation (`locale-negotiation.ts`) is what it is for.
|
|
68
|
+
*
|
|
69
|
+
* @param {readonly { urlPattern: string }[]} routes
|
|
70
|
+
* @param {ResolvedI18n | null} i18n
|
|
71
|
+
* @returns {string[]} Offending URL patterns, in route order
|
|
72
|
+
*/
|
|
73
|
+
export function unprefixedRoutes(
|
|
74
|
+
routes: readonly { urlPattern: string }[],
|
|
75
|
+
i18n: ResolvedI18n | null,
|
|
76
|
+
): string[] {
|
|
77
|
+
if (i18n === null || i18n.routing !== "prefix-always") {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const offenders: string[] = [];
|
|
81
|
+
for (const route of routes) {
|
|
82
|
+
if (route.urlPattern === "/") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const first = route.urlPattern.split("/").find(Boolean);
|
|
86
|
+
const canonical = canonicalizeLocale(first);
|
|
87
|
+
if (canonical === null || !i18n.locales.includes(canonical)) {
|
|
88
|
+
offenders.push(route.urlPattern);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return offenders;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A route prefix that looks like one of the declared locales but is not one of them.
|
|
96
|
+
*
|
|
97
|
+
* The precise mistake this catches: `i18n.locales` says `fr-CA` and the pages live in `pages/fr/`.
|
|
98
|
+
* The segment is a well-formed tag, the directory is obviously meant to be French, and nothing
|
|
99
|
+
* matches — so every page under it silently becomes the default locale and claims the wrong
|
|
100
|
+
* language in `<html lang>`. Left alone that is invisible; there is no error and the page looks
|
|
101
|
+
* fine.
|
|
102
|
+
*
|
|
103
|
+
* Scoped to the **primary language** of a declared locale on purpose. Any 2-to-8-letter segment is
|
|
104
|
+
* a well-formed language tag — `/docs/`, `/api/`, `/it/` — so warning on "looks like a tag" would
|
|
105
|
+
* fire on ordinary paths. Warning on "looks like a locale you declared" fires only on the mistake.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} urlPattern
|
|
108
|
+
* @param {ResolvedI18n | null} i18n
|
|
109
|
+
* @returns {{ segment: string; meant: string } | null}
|
|
110
|
+
*/
|
|
111
|
+
export function undeclaredLocalePrefix(
|
|
112
|
+
urlPattern: string,
|
|
113
|
+
i18n: ResolvedI18n | null,
|
|
114
|
+
): { segment: string; meant: string } | null {
|
|
115
|
+
if (i18n === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const first = urlPattern.split("/").find(Boolean);
|
|
119
|
+
const canonical = canonicalizeLocale(first);
|
|
120
|
+
if (first === undefined || canonical === null || i18n.locales.includes(canonical)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const meant = i18n.locales.find(
|
|
124
|
+
(locale) => locale !== canonical && locale.split("-")[0] === canonical.split("-")[0],
|
|
125
|
+
);
|
|
126
|
+
return meant === undefined ? null : { meant, segment: first };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A route's identity across locales: its path with the locale prefix removed.
|
|
131
|
+
*
|
|
132
|
+
* `/fr-ca/about/` and `/about/` share the key `about/`, which is what makes them translations of
|
|
133
|
+
* one another. The directory layout is the default mapping, and it is enough whenever the paths are
|
|
134
|
+
* parallel.
|
|
135
|
+
*
|
|
136
|
+
* A **localized slug** is not parallel, and that is the case this cannot derive: `/fr-ca/a-propos/`
|
|
137
|
+
* shares nothing with `/about/`. A document says so itself with `$translationKey`, which overrides
|
|
138
|
+
* this exactly as `$lang` overrides the locale its route implies (§13.4) — one key, and the whole
|
|
139
|
+
* annotation follows.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} urlPattern
|
|
142
|
+
* @param {ResolvedI18n | null} i18n
|
|
143
|
+
* @returns {string}
|
|
144
|
+
*/
|
|
145
|
+
export function translationKey(urlPattern: string, i18n: ResolvedI18n | null): string {
|
|
146
|
+
const segments = urlPattern.split("/").filter(Boolean);
|
|
147
|
+
const canonical = canonicalizeLocale(segments[0]);
|
|
148
|
+
if (i18n !== null && canonical !== null && i18n.locales.includes(canonical)) {
|
|
149
|
+
return segments.slice(1).join("/");
|
|
150
|
+
}
|
|
151
|
+
return segments.join("/");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One `<link rel="alternate">` / sitemap `xhtml:link`. */
|
|
155
|
+
export interface LocaleAlternate {
|
|
156
|
+
hreflang: string;
|
|
157
|
+
href: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One route in a translation set: the locale it serves, and where it lives. */
|
|
161
|
+
export interface TranslationMember {
|
|
162
|
+
/** Canonical BCP 47 tag. */
|
|
163
|
+
locale: string;
|
|
164
|
+
/** Site-absolute route pattern, e.g. `/fr-ca/about/`. */
|
|
165
|
+
urlPattern: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A route as translation grouping sees it: its URL, and the key its document declared. */
|
|
169
|
+
export interface TranslationRoute {
|
|
170
|
+
/** From the document's `$translationKey`; absent when the key is derived from the path. */
|
|
171
|
+
translationKey?: string | undefined;
|
|
172
|
+
urlPattern: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Two routes claiming to be the same page in the same language. */
|
|
176
|
+
export interface TranslationConflict {
|
|
177
|
+
/** True when at least one of them said so with `$translationKey`. */
|
|
178
|
+
declared: boolean;
|
|
179
|
+
key: string;
|
|
180
|
+
locale: string;
|
|
181
|
+
/** In route order; the first is the one that keeps the set. */
|
|
182
|
+
urlPatterns: string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Everything a page needs to know about its own locale, gathered once per build. */
|
|
186
|
+
export interface PageLocaleContext {
|
|
187
|
+
/** Absolute alternates for `<head>` and the sitemap; empty without a site `url`. */
|
|
188
|
+
alternates: readonly LocaleAlternate[];
|
|
189
|
+
i18n: ResolvedI18n | null;
|
|
190
|
+
/** The page's translation set, site-absolute and including itself. */
|
|
191
|
+
translations: readonly TranslationMember[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Group routes into translation sets: for each route, every route that is the same page in another
|
|
196
|
+
* language, itself included, ordered by tag.
|
|
197
|
+
*
|
|
198
|
+
* One derivation, two readers. {@link localeAlternates} turns a set into absolute `<link
|
|
199
|
+
* rel="alternate">` hrefs for crawlers; `injectContext` turns it into `$page.alternates` for a
|
|
200
|
+
* language switcher a reader can click. Deriving it twice is how the two would come to disagree
|
|
201
|
+
* about which pages are translations of one another — and the disagreement would be invisible,
|
|
202
|
+
* because one of them is only ever read by a machine.
|
|
203
|
+
*
|
|
204
|
+
* **A set of one is kept here and dropped there.** A lone `hreflang` pointing at itself is noise in
|
|
205
|
+
* `<head>` (§13.5), but a template asking "which languages is this page in" wants the honest
|
|
206
|
+
* answer, and dropping the page itself would leave a switcher unable to mark where the reader is.
|
|
207
|
+
*
|
|
208
|
+
* URLs stay **site-absolute** rather than becoming absolute hrefs, because a switcher is a link
|
|
209
|
+
* within the site and works before `url` is configured — which is every project during
|
|
210
|
+
* development.
|
|
211
|
+
*
|
|
212
|
+
* @param {readonly TranslationRoute[]} routes - Concrete routes only
|
|
213
|
+
* @param {ResolvedI18n | null} i18n
|
|
214
|
+
* @returns {{ conflicts: TranslationConflict[]; sets: Map<string, TranslationMember[]> }} Sets are
|
|
215
|
+
* keyed by `urlPattern`; members of one set share one array
|
|
216
|
+
*/
|
|
217
|
+
export function translationSets(
|
|
218
|
+
routes: readonly TranslationRoute[],
|
|
219
|
+
i18n: ResolvedI18n | null,
|
|
220
|
+
): { conflicts: TranslationConflict[]; sets: Map<string, TranslationMember[]> } {
|
|
221
|
+
const out = new Map<string, TranslationMember[]>();
|
|
222
|
+
const conflicts: TranslationConflict[] = [];
|
|
223
|
+
if (i18n === null) {
|
|
224
|
+
return { conflicts, sets: out };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sets = new Map<string, TranslationMember[]>();
|
|
228
|
+
const declared = new Set<string>();
|
|
229
|
+
for (const route of routes) {
|
|
230
|
+
/*
|
|
231
|
+
* Slashes at the ends are trimmed so a declared key can be written the way the URL reads.
|
|
232
|
+
* The derived form has none — `/about/` reduces to `about` — and a `"/about"` that silently
|
|
233
|
+
* matched nothing would look exactly like a key that was ignored.
|
|
234
|
+
*/
|
|
235
|
+
const key =
|
|
236
|
+
route.translationKey === undefined
|
|
237
|
+
? translationKey(route.urlPattern, i18n)
|
|
238
|
+
: route.translationKey.replaceAll(/^\/+|\/+$/g, "");
|
|
239
|
+
if (route.translationKey !== undefined) {
|
|
240
|
+
declared.add(route.urlPattern);
|
|
241
|
+
}
|
|
242
|
+
const locale = localeOfRoute(route.urlPattern, i18n);
|
|
243
|
+
if (locale === null) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const members = sets.get(key) ?? [];
|
|
247
|
+
/*
|
|
248
|
+
* A duplicate locale in one set means two routes claim to be the same page in one language.
|
|
249
|
+
* The first wins, which keeps the set single-valued rather than advertising a contradiction;
|
|
250
|
+
* the loser is keyed nowhere, so it carries no alternates and no switcher.
|
|
251
|
+
*
|
|
252
|
+
* How loudly to say so is the caller's decision, and it turns on which kind of collision it
|
|
253
|
+
* is: a declared one is a promise the author wrote down and broke, while a derived one may be
|
|
254
|
+
* a deliberate alias the compiler cannot see the reason for.
|
|
255
|
+
*/
|
|
256
|
+
const taken = members.find((m) => m.locale === locale);
|
|
257
|
+
if (taken === undefined) {
|
|
258
|
+
members.push({ locale, urlPattern: route.urlPattern });
|
|
259
|
+
} else {
|
|
260
|
+
conflicts.push({
|
|
261
|
+
declared: declared.has(taken.urlPattern) || route.translationKey !== undefined,
|
|
262
|
+
key,
|
|
263
|
+
locale,
|
|
264
|
+
urlPatterns: [taken.urlPattern, route.urlPattern],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
sets.set(key, members);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const members of sets.values()) {
|
|
271
|
+
const ordered = members.toSorted((a, b) =>
|
|
272
|
+
a.locale === b.locale ? 0 : a.locale < b.locale ? -1 : 1,
|
|
273
|
+
);
|
|
274
|
+
for (const member of members) {
|
|
275
|
+
out.set(member.urlPattern, ordered);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { conflicts, sets: out };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Group routes into translation sets and give each one its alternates.
|
|
283
|
+
*
|
|
284
|
+
* A page with no translations gets **none** — a lone `hreflang` pointing at itself is noise, and
|
|
285
|
+
* the standards it would satisfy expect a set. `x-default` names the default locale's URL, which is
|
|
286
|
+
* the convention search engines act on for "the page to send an unmatched visitor to"; it is
|
|
287
|
+
* omitted when the set has no default-locale member, since inventing one would advertise a URL that
|
|
288
|
+
* does not exist.
|
|
289
|
+
*
|
|
290
|
+
* Reciprocity is automatic: every member of a set lists every member **including itself**, which is
|
|
291
|
+
* what the annotation is specified to do and what validators check for.
|
|
292
|
+
*
|
|
293
|
+
* @param {readonly TranslationRoute[]} routes - Concrete routes only
|
|
294
|
+
* @param {ResolvedI18n | null} i18n
|
|
295
|
+
* @param {string} siteUrl - Absolute site URL; alternates must be absolute
|
|
296
|
+
* @returns {Map<string, LocaleAlternate[]>} Keyed by `urlPattern`
|
|
297
|
+
*/
|
|
298
|
+
export function localeAlternates(
|
|
299
|
+
routes: readonly TranslationRoute[],
|
|
300
|
+
i18n: ResolvedI18n | null,
|
|
301
|
+
siteUrl: string,
|
|
302
|
+
): Map<string, LocaleAlternate[]> {
|
|
303
|
+
const out = new Map<string, LocaleAlternate[]>();
|
|
304
|
+
if (i18n === null || siteUrl === "") {
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Built once per set rather than once per member: every member advertises the same list. */
|
|
309
|
+
const built = new Map<readonly TranslationMember[], LocaleAlternate[]>();
|
|
310
|
+
for (const [urlPattern, members] of translationSets(routes, i18n).sets) {
|
|
311
|
+
if (members.length < 2) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
let alternates = built.get(members);
|
|
315
|
+
if (alternates === undefined) {
|
|
316
|
+
alternates = members.map((m) => ({
|
|
317
|
+
href: new URL(m.urlPattern, siteUrl).href,
|
|
318
|
+
hreflang: m.locale,
|
|
319
|
+
}));
|
|
320
|
+
const fallback = members.find((m) => m.locale === i18n.defaultLocale);
|
|
321
|
+
if (fallback !== undefined) {
|
|
322
|
+
alternates.push({
|
|
323
|
+
href: new URL(fallback.urlPattern, siteUrl).href,
|
|
324
|
+
hreflang: "x-default",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
built.set(members, alternates);
|
|
328
|
+
}
|
|
329
|
+
out.set(urlPattern, alternates);
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The `lang` and `dir` a page ships with.
|
|
336
|
+
*
|
|
337
|
+
* An explicit `$lang` on the document wins over the locale its route implies: a page really can be
|
|
338
|
+
* a French translation living at `/en/a-propos/`, and the author who wrote that down means it.
|
|
339
|
+
*
|
|
340
|
+
* `dir` is emitted only when it is `rtl` or when the author asked for something. `ltr` is the
|
|
341
|
+
* default for every element in HTML, so writing it on every page of a left-to-right site is noise
|
|
342
|
+
* that says nothing.
|
|
343
|
+
*
|
|
344
|
+
* @param {object} args
|
|
345
|
+
* @returns {{ lang: string; dir?: TextDirection | string }}
|
|
346
|
+
*/
|
|
347
|
+
export function pageLanguage({
|
|
348
|
+
pageLang,
|
|
349
|
+
pageDir,
|
|
350
|
+
routeLocale,
|
|
351
|
+
defaults,
|
|
352
|
+
}: {
|
|
353
|
+
pageLang?: string | undefined;
|
|
354
|
+
pageDir?: string | undefined;
|
|
355
|
+
routeLocale?: string | null;
|
|
356
|
+
defaults?: { lang?: string | undefined; dir?: string | undefined } | undefined;
|
|
357
|
+
}): { lang: string; dir?: string } {
|
|
358
|
+
const lang = pageLang ?? routeLocale ?? defaults?.lang ?? "en";
|
|
359
|
+
const explicit = pageDir ?? defaults?.dir;
|
|
360
|
+
if (explicit !== undefined) {
|
|
361
|
+
return { dir: explicit, lang };
|
|
362
|
+
}
|
|
363
|
+
const derived: TextDirection = localeDirection(lang);
|
|
364
|
+
return derived === "rtl" ? { dir: "rtl", lang } : { lang };
|
|
365
|
+
}
|
|
@@ -33,6 +33,13 @@ export interface ImageConfig {
|
|
|
33
33
|
quality: { webp?: number; avif?: number; jpeg?: number; png?: number };
|
|
34
34
|
sizes: string;
|
|
35
35
|
lazyLoad: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a multi-format image in a `<picture>` with one `<source>` per format. Default true.
|
|
38
|
+
* Setting it false restores a bare `<img srcset>` in the preferred format, at the cost of
|
|
39
|
+
* browsers that cannot decode that format: `srcset` carries no type information, so they pick an
|
|
40
|
+
* undecodable candidate rather than falling back.
|
|
41
|
+
*/
|
|
42
|
+
picture?: boolean;
|
|
36
43
|
service?: "build" | "cloudflare";
|
|
37
44
|
/**
|
|
38
45
|
* Hostnames whose remote (https) images are routed through the optimization pipeline. Cloudflare
|