@richie-router/server 0.1.3 → 0.1.5
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/cjs/index.cjs +287 -46
- package/dist/cjs/index.test.cjs +335 -0
- package/dist/esm/index.mjs +288 -46
- package/dist/esm/index.test.mjs +335 -0
- package/dist/types/index.d.ts +40 -2
- package/package.json +2 -2
package/dist/esm/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
defaultStringifySearch,
|
|
7
7
|
isNotFound,
|
|
8
8
|
isRedirect,
|
|
9
|
+
matchPathname,
|
|
9
10
|
matchRouteTree,
|
|
10
11
|
resolveHeadConfig,
|
|
11
12
|
serializeHeadConfig
|
|
@@ -19,32 +20,143 @@ function defineHeadTags(routeManifest, routerSchema, definitions) {
|
|
|
19
20
|
}
|
|
20
21
|
var HEAD_PLACEHOLDER = "<!--richie-router-head-->";
|
|
21
22
|
var MANAGED_HEAD_ATTRIBUTE = "data-richie-router-head";
|
|
23
|
+
var HEAD_RESPONSE_KIND_HEADER = "x-richie-router-head";
|
|
24
|
+
var EMPTY_HEAD = [];
|
|
25
|
+
function ensureLeadingSlash(value) {
|
|
26
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
27
|
+
}
|
|
28
|
+
function normalizeBasePath(basePath) {
|
|
29
|
+
if (!basePath) {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
const trimmed = basePath.trim();
|
|
33
|
+
if (trimmed === "" || trimmed === "/") {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
const normalized = ensureLeadingSlash(trimmed).replace(/\/+$/u, "");
|
|
37
|
+
return normalized === "/" ? "" : normalized;
|
|
38
|
+
}
|
|
39
|
+
function stripBasePathFromPathname(pathname, basePath) {
|
|
40
|
+
if (!basePath) {
|
|
41
|
+
return pathname;
|
|
42
|
+
}
|
|
43
|
+
if (pathname === basePath) {
|
|
44
|
+
return "/";
|
|
45
|
+
}
|
|
46
|
+
if (pathname.startsWith(`${basePath}/`)) {
|
|
47
|
+
return pathname.slice(basePath.length) || "/";
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function prependBasePathToPathname(pathname, basePath) {
|
|
52
|
+
if (!basePath) {
|
|
53
|
+
return pathname;
|
|
54
|
+
}
|
|
55
|
+
return pathname === "/" ? basePath : `${basePath}${ensureLeadingSlash(pathname)}`;
|
|
56
|
+
}
|
|
22
57
|
function routeHasRecord(value) {
|
|
23
58
|
return typeof value === "object" && value !== null;
|
|
24
59
|
}
|
|
25
|
-
function createHeadSnapshotScript(href, head) {
|
|
26
|
-
const payload = JSON.stringify({ href, head }).replaceAll("</script>", "<\\/script>");
|
|
60
|
+
function createHeadSnapshotScript(href, head, routeHeads) {
|
|
61
|
+
const payload = JSON.stringify({ href, head, routeHeads }).replaceAll("</script>", "<\\/script>");
|
|
27
62
|
return `<script ${MANAGED_HEAD_ATTRIBUTE}="true">window.__RICHIE_ROUTER_HEAD__=${payload}</script>`;
|
|
28
63
|
}
|
|
29
|
-
|
|
64
|
+
function createRichieRouterHead(href, head, routeHeads) {
|
|
65
|
+
const headHtml = serializeHeadConfig(head, {
|
|
66
|
+
managedAttribute: MANAGED_HEAD_ATTRIBUTE
|
|
67
|
+
});
|
|
68
|
+
return `${headHtml}${createHeadSnapshotScript(href, head, routeHeads)}`;
|
|
69
|
+
}
|
|
70
|
+
async function renderTemplate(html, ctx, options) {
|
|
30
71
|
const template = html.template;
|
|
31
72
|
if (typeof template === "function") {
|
|
32
73
|
return await template(ctx);
|
|
33
74
|
}
|
|
34
75
|
if (!template.includes(HEAD_PLACEHOLDER)) {
|
|
76
|
+
if (options?.requireHeadPlaceholder === false) {
|
|
77
|
+
return template;
|
|
78
|
+
}
|
|
35
79
|
throw new Error(`HTML template is missing required Richie Router placeholder: ${HEAD_PLACEHOLDER}`);
|
|
36
80
|
}
|
|
37
81
|
return template.replace(HEAD_PLACEHOLDER, ctx.richieRouterHead);
|
|
38
82
|
}
|
|
39
83
|
function jsonResponse(data, init) {
|
|
84
|
+
const headers = new Headers(init?.headers);
|
|
85
|
+
if (!headers.has("content-type")) {
|
|
86
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
87
|
+
}
|
|
40
88
|
return new Response(JSON.stringify(data), {
|
|
41
89
|
...init,
|
|
42
|
-
headers
|
|
43
|
-
"content-type": "application/json; charset=utf-8",
|
|
44
|
-
...init?.headers ?? {}
|
|
45
|
-
}
|
|
90
|
+
headers
|
|
46
91
|
});
|
|
47
92
|
}
|
|
93
|
+
function notFoundResult() {
|
|
94
|
+
return {
|
|
95
|
+
matched: false,
|
|
96
|
+
response: new Response("Not Found", { status: 404 })
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function htmlResponse(html, headers) {
|
|
100
|
+
const responseHeaders = new Headers(headers);
|
|
101
|
+
if (!responseHeaders.has("content-type")) {
|
|
102
|
+
responseHeaders.set("content-type", "text/html; charset=utf-8");
|
|
103
|
+
}
|
|
104
|
+
return new Response(html, {
|
|
105
|
+
status: 200,
|
|
106
|
+
headers: responseHeaders
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function withResponseHeaders(response, headersToSet) {
|
|
110
|
+
const headers = new Headers(response.headers);
|
|
111
|
+
new Headers(headersToSet).forEach((value, key) => {
|
|
112
|
+
headers.set(key, value);
|
|
113
|
+
});
|
|
114
|
+
return new Response(response.body, {
|
|
115
|
+
status: response.status,
|
|
116
|
+
statusText: response.statusText,
|
|
117
|
+
headers
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function formatHeadCacheControl(staleTime) {
|
|
121
|
+
if (staleTime === undefined) {
|
|
122
|
+
return "private, no-store";
|
|
123
|
+
}
|
|
124
|
+
return `private, max-age=${Math.max(0, Math.floor(staleTime / 1000))}`;
|
|
125
|
+
}
|
|
126
|
+
function createHeadResponseHeaders(kind, staleTime, headers) {
|
|
127
|
+
const responseHeaders = new Headers(headers);
|
|
128
|
+
responseHeaders.set(HEAD_RESPONSE_KIND_HEADER, kind);
|
|
129
|
+
responseHeaders.set("cache-control", formatHeadCacheControl(staleTime));
|
|
130
|
+
return responseHeaders;
|
|
131
|
+
}
|
|
132
|
+
function resolveDocumentRequest(request, basePathOption) {
|
|
133
|
+
return resolveDocumentPath(request.url, basePathOption);
|
|
134
|
+
}
|
|
135
|
+
function resolveDocumentPath(path, basePathOption) {
|
|
136
|
+
const url = new URL(path, "http://richie-router.local");
|
|
137
|
+
const basePath = normalizeBasePath(basePathOption);
|
|
138
|
+
const strippedPathname = stripBasePathFromPathname(url.pathname, basePath);
|
|
139
|
+
if (strippedPathname === null) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
basePath,
|
|
144
|
+
location: createParsedLocation(`${strippedPathname}${url.search}${url.hash}`, null, defaultParseSearch)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function renderDocumentResponse(request, html, richieRouterHead, head, options) {
|
|
148
|
+
const template = await renderTemplate(html, {
|
|
149
|
+
request,
|
|
150
|
+
richieRouterHead,
|
|
151
|
+
head
|
|
152
|
+
}, {
|
|
153
|
+
requireHeadPlaceholder: options?.requireHeadPlaceholder
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
matched: true,
|
|
157
|
+
response: htmlResponse(template, options?.headers)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
48
160
|
function resolveSearch(route, rawSearch) {
|
|
49
161
|
const fromSchema = route.searchSchema ? route.searchSchema.parse(rawSearch) : {};
|
|
50
162
|
if (routeHasRecord(fromSchema)) {
|
|
@@ -77,6 +189,29 @@ function buildMatches(routeManifest, location) {
|
|
|
77
189
|
};
|
|
78
190
|
});
|
|
79
191
|
}
|
|
192
|
+
function resolveSpaRoutes(spaRoutesManifest) {
|
|
193
|
+
if (!routeHasRecord(spaRoutesManifest)) {
|
|
194
|
+
throw new Error("Invalid spaRoutesManifest: expected an object.");
|
|
195
|
+
}
|
|
196
|
+
const { spaRoutes } = spaRoutesManifest;
|
|
197
|
+
if (!Array.isArray(spaRoutes) || spaRoutes.some((route) => typeof route !== "string")) {
|
|
198
|
+
throw new Error('Invalid spaRoutesManifest: expected "spaRoutes" to be an array of strings.');
|
|
199
|
+
}
|
|
200
|
+
return spaRoutes;
|
|
201
|
+
}
|
|
202
|
+
function matchesSpaLocation(options, location) {
|
|
203
|
+
if ("routeManifest" in options) {
|
|
204
|
+
return buildMatches(options.routeManifest, location).length > 0;
|
|
205
|
+
}
|
|
206
|
+
return resolveSpaRoutes(options.spaRoutesManifest).some((route) => matchPathname(route, location.pathname) !== null);
|
|
207
|
+
}
|
|
208
|
+
function matchesSpaPath(path, options) {
|
|
209
|
+
const documentRequest = resolveDocumentPath(path, options.basePath);
|
|
210
|
+
if (documentRequest === null) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return matchesSpaLocation(options, documentRequest.location);
|
|
214
|
+
}
|
|
80
215
|
async function executeHeadTag(request, headTags, routeId, params, rawSearch) {
|
|
81
216
|
const definition = headTags.definitions[routeId];
|
|
82
217
|
const schemaEntry = headTags.routerSchema[routeId];
|
|
@@ -96,29 +231,132 @@ async function executeHeadTag(request, headTags, routeId, params, rawSearch) {
|
|
|
96
231
|
}
|
|
97
232
|
async function resolveMatchedHead(request, headTags, matches) {
|
|
98
233
|
const resolvedHeadByRoute = new Map;
|
|
234
|
+
const routeHeads = [];
|
|
235
|
+
let staleTime;
|
|
99
236
|
for (const match of matches) {
|
|
100
237
|
if (!match.route.serverHead) {
|
|
101
238
|
continue;
|
|
102
239
|
}
|
|
103
240
|
const result = await executeHeadTag(request, headTags, match.route.fullPath, match.params, match.search);
|
|
104
241
|
resolvedHeadByRoute.set(match.route.fullPath, result.head);
|
|
242
|
+
routeHeads.push({
|
|
243
|
+
routeId: match.route.fullPath,
|
|
244
|
+
head: result.head,
|
|
245
|
+
staleTime: result.staleTime
|
|
246
|
+
});
|
|
247
|
+
if (result.staleTime !== undefined) {
|
|
248
|
+
staleTime = staleTime === undefined ? result.staleTime : Math.min(staleTime, result.staleTime);
|
|
249
|
+
}
|
|
105
250
|
}
|
|
106
|
-
return
|
|
251
|
+
return {
|
|
252
|
+
head: resolveHeadConfig(matches, resolvedHeadByRoute),
|
|
253
|
+
routeHeads,
|
|
254
|
+
staleTime
|
|
255
|
+
};
|
|
107
256
|
}
|
|
108
|
-
|
|
257
|
+
function createDocumentHeadRequest(sourceRequest, href) {
|
|
258
|
+
const requestUrl = new URL(sourceRequest.url);
|
|
259
|
+
const targetUrl = new URL(href, requestUrl);
|
|
260
|
+
return new Request(targetUrl.toString(), {
|
|
261
|
+
method: "GET",
|
|
262
|
+
headers: sourceRequest.headers
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function handleDocumentHeadRequest(request, options, href) {
|
|
266
|
+
const documentRequest = createDocumentHeadRequest(request, href);
|
|
267
|
+
const resolvedDocumentRequest = resolveDocumentRequest(documentRequest, options.basePath);
|
|
268
|
+
if (resolvedDocumentRequest === null) {
|
|
269
|
+
return {
|
|
270
|
+
matched: true,
|
|
271
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
272
|
+
status: 404,
|
|
273
|
+
headers: createHeadResponseHeaders("document")
|
|
274
|
+
})
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const matches = buildMatches(options.headTags.routeManifest, resolvedDocumentRequest.location);
|
|
279
|
+
if (matches.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
matched: true,
|
|
282
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
283
|
+
status: 404,
|
|
284
|
+
headers: createHeadResponseHeaders("document")
|
|
285
|
+
})
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const { head, routeHeads, staleTime } = await resolveMatchedHead(documentRequest, options.headTags, matches);
|
|
289
|
+
return {
|
|
290
|
+
matched: true,
|
|
291
|
+
response: jsonResponse({
|
|
292
|
+
href: resolvedDocumentRequest.location.href,
|
|
293
|
+
head,
|
|
294
|
+
routeHeads,
|
|
295
|
+
staleTime,
|
|
296
|
+
richieRouterHead: createRichieRouterHead(resolvedDocumentRequest.location.href, head, routeHeads)
|
|
297
|
+
}, {
|
|
298
|
+
headers: createHeadResponseHeaders("document", staleTime)
|
|
299
|
+
})
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (isRedirect(error)) {
|
|
303
|
+
const redirectPath = prependBasePathToPathname(buildPath(error.options.to, error.options.params ?? {}), resolvedDocumentRequest.basePath);
|
|
304
|
+
const redirectSearch = defaultStringifySearch(error.options.search === true ? {} : error.options.search ?? {});
|
|
305
|
+
const redirectHash = error.options.hash ? `#${error.options.hash.replace(/^#/, "")}` : "";
|
|
306
|
+
const redirectUrl = `${redirectPath}${redirectSearch}${redirectHash}`;
|
|
307
|
+
return {
|
|
308
|
+
matched: true,
|
|
309
|
+
response: new Response(null, {
|
|
310
|
+
status: error.options.replace ? 307 : 302,
|
|
311
|
+
headers: createHeadResponseHeaders("document", undefined, {
|
|
312
|
+
location: redirectUrl
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (error instanceof Response) {
|
|
318
|
+
return {
|
|
319
|
+
matched: true,
|
|
320
|
+
response: withResponseHeaders(error, createHeadResponseHeaders("document"))
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (isNotFound(error)) {
|
|
324
|
+
return {
|
|
325
|
+
matched: true,
|
|
326
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
327
|
+
status: 404,
|
|
328
|
+
headers: createHeadResponseHeaders("document")
|
|
329
|
+
})
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function handleHeadRequest(request, options) {
|
|
109
336
|
const url = new URL(request.url);
|
|
110
|
-
const
|
|
337
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
338
|
+
const headBasePath = options.headBasePath ?? prependBasePathToPathname("/head-api", basePath);
|
|
111
339
|
if (url.pathname !== headBasePath) {
|
|
112
340
|
return {
|
|
113
341
|
matched: false,
|
|
114
342
|
response: new Response("Not Found", { status: 404 })
|
|
115
343
|
};
|
|
116
344
|
}
|
|
345
|
+
const href = url.searchParams.get("href");
|
|
346
|
+
if (href !== null) {
|
|
347
|
+
return await handleDocumentHeadRequest(request, {
|
|
348
|
+
...options,
|
|
349
|
+
basePath
|
|
350
|
+
}, href);
|
|
351
|
+
}
|
|
117
352
|
const routeId = url.searchParams.get("routeId");
|
|
118
353
|
if (!routeId) {
|
|
119
354
|
return {
|
|
120
355
|
matched: true,
|
|
121
|
-
response: jsonResponse({ message: "Missing routeId" }, {
|
|
356
|
+
response: jsonResponse({ message: "Missing routeId" }, {
|
|
357
|
+
status: 400,
|
|
358
|
+
headers: createHeadResponseHeaders("route")
|
|
359
|
+
})
|
|
122
360
|
};
|
|
123
361
|
}
|
|
124
362
|
const params = JSON.parse(url.searchParams.get("params") ?? "{}");
|
|
@@ -127,71 +365,72 @@ async function handleHeadTagRequest(request, options) {
|
|
|
127
365
|
const result = await executeHeadTag(request, options.headTags, routeId, params, search);
|
|
128
366
|
return {
|
|
129
367
|
matched: true,
|
|
130
|
-
response: jsonResponse(result
|
|
368
|
+
response: jsonResponse(result, {
|
|
369
|
+
headers: createHeadResponseHeaders("route", result.staleTime)
|
|
370
|
+
})
|
|
131
371
|
};
|
|
132
372
|
} catch (error) {
|
|
133
373
|
if (error instanceof Response) {
|
|
134
374
|
return {
|
|
135
375
|
matched: true,
|
|
136
|
-
response: error
|
|
376
|
+
response: withResponseHeaders(error, createHeadResponseHeaders("route"))
|
|
137
377
|
};
|
|
138
378
|
}
|
|
139
379
|
if (isNotFound(error)) {
|
|
140
380
|
return {
|
|
141
381
|
matched: true,
|
|
142
|
-
response: jsonResponse({ message: "Not Found" }, {
|
|
382
|
+
response: jsonResponse({ message: "Not Found" }, {
|
|
383
|
+
status: 404,
|
|
384
|
+
headers: createHeadResponseHeaders("route")
|
|
385
|
+
})
|
|
143
386
|
};
|
|
144
387
|
}
|
|
145
388
|
throw error;
|
|
146
389
|
}
|
|
147
390
|
}
|
|
391
|
+
async function handleHeadTagRequest(request, options) {
|
|
392
|
+
return await handleHeadRequest(request, options);
|
|
393
|
+
}
|
|
394
|
+
async function handleSpaRequest(request, options) {
|
|
395
|
+
const documentRequest = resolveDocumentRequest(request, options.basePath);
|
|
396
|
+
if (documentRequest === null) {
|
|
397
|
+
return notFoundResult();
|
|
398
|
+
}
|
|
399
|
+
if (!matchesSpaLocation(options, documentRequest.location)) {
|
|
400
|
+
return notFoundResult();
|
|
401
|
+
}
|
|
402
|
+
return await renderDocumentResponse(request, options.html, "", EMPTY_HEAD, {
|
|
403
|
+
headers: options.headers,
|
|
404
|
+
requireHeadPlaceholder: false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
148
407
|
async function handleRequest(request, options) {
|
|
149
|
-
const
|
|
150
|
-
const routeBasePath = options.routeBasePath ?? "/";
|
|
408
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
151
409
|
const handledHeadTagRequest = await handleHeadTagRequest(request, {
|
|
152
410
|
headTags: options.headTags,
|
|
411
|
+
basePath,
|
|
153
412
|
headBasePath: options.headBasePath
|
|
154
413
|
});
|
|
155
414
|
if (handledHeadTagRequest.matched) {
|
|
156
415
|
return handledHeadTagRequest;
|
|
157
416
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
response: new Response("Not Found", { status: 404 })
|
|
162
|
-
};
|
|
417
|
+
const documentRequest = resolveDocumentRequest(request, basePath);
|
|
418
|
+
if (documentRequest === null) {
|
|
419
|
+
return notFoundResult();
|
|
163
420
|
}
|
|
164
|
-
const
|
|
165
|
-
const matches = buildMatches(options.routeManifest, location);
|
|
421
|
+
const matches = buildMatches(options.routeManifest, documentRequest.location);
|
|
166
422
|
if (matches.length === 0) {
|
|
167
|
-
return
|
|
168
|
-
matched: false,
|
|
169
|
-
response: new Response("Not Found", { status: 404 })
|
|
170
|
-
};
|
|
423
|
+
return notFoundResult();
|
|
171
424
|
}
|
|
172
425
|
try {
|
|
173
|
-
const head = await resolveMatchedHead(request, options.headTags, matches);
|
|
174
|
-
const
|
|
175
|
-
|
|
426
|
+
const { head, routeHeads } = await resolveMatchedHead(request, options.headTags, matches);
|
|
427
|
+
const richieRouterHead = createRichieRouterHead(documentRequest.location.href, head, routeHeads);
|
|
428
|
+
return await renderDocumentResponse(request, options.html, richieRouterHead, head, {
|
|
429
|
+
headers: options.headers
|
|
176
430
|
});
|
|
177
|
-
const richieRouterHead = `${headHtml}${createHeadSnapshotScript(location.href, head)}`;
|
|
178
|
-
const html = await renderTemplate(options.html, {
|
|
179
|
-
request,
|
|
180
|
-
richieRouterHead,
|
|
181
|
-
head
|
|
182
|
-
});
|
|
183
|
-
return {
|
|
184
|
-
matched: true,
|
|
185
|
-
response: new Response(html, {
|
|
186
|
-
status: 200,
|
|
187
|
-
headers: {
|
|
188
|
-
"content-type": "text/html; charset=utf-8"
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
};
|
|
192
431
|
} catch (error) {
|
|
193
432
|
if (isRedirect(error)) {
|
|
194
|
-
const redirectPath = buildPath(error.options.to, error.options.params ?? {});
|
|
433
|
+
const redirectPath = prependBasePathToPathname(buildPath(error.options.to, error.options.params ?? {}), documentRequest.basePath);
|
|
195
434
|
const redirectSearch = defaultStringifySearch(error.options.search === true ? {} : error.options.search ?? {});
|
|
196
435
|
const redirectHash = error.options.hash ? `#${error.options.hash.replace(/^#/, "")}` : "";
|
|
197
436
|
const redirectUrl = `${redirectPath}${redirectSearch}${redirectHash}`;
|
|
@@ -221,7 +460,10 @@ async function handleRequest(request, options) {
|
|
|
221
460
|
}
|
|
222
461
|
}
|
|
223
462
|
export {
|
|
463
|
+
matchesSpaPath,
|
|
464
|
+
handleSpaRequest,
|
|
224
465
|
handleRequest,
|
|
225
466
|
handleHeadTagRequest,
|
|
467
|
+
handleHeadRequest,
|
|
226
468
|
defineHeadTags
|
|
227
469
|
};
|