@transclude/core 0.11.3 → 0.12.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/bin/build.js +1 -0
- package/bin/dev.js +1 -0
- package/package.json +1 -1
- package/src/app.js +2 -0
- package/src/compiler/bind.js +36 -4
- package/src/compiler/codegen.js +59 -14
- package/src/compiler/index.js +2 -1
- package/src/defaults.js +58 -1
- package/src/document.js +38 -10
- package/src/runtime/index.js +14 -0
- package/src/server.js +3 -1
package/bin/build.js
CHANGED
package/bin/dev.js
CHANGED
|
@@ -191,6 +191,7 @@ const renderPage = async (route, c, status = null, extra = {}) => {
|
|
|
191
191
|
stylesheet: config.stylesheet ? `/${config.stylesheet}` : null,
|
|
192
192
|
csp: config.csp,
|
|
193
193
|
lang: config.lang,
|
|
194
|
+
canonical: config.canonical,
|
|
194
195
|
include,
|
|
195
196
|
});
|
|
196
197
|
// A loader answered for itself: a redirect, or something that is not a page.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transclude/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "An HTML-first server framework. A page is an .html file, the directory tree is the route table, and any fragment of a page is a URL of its own. Runs on Node, Bun, Deno and workerd, and ships no client JavaScript by default.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
package/src/app.js
CHANGED
|
@@ -358,6 +358,7 @@ export function createApp({
|
|
|
358
358
|
stylesheet: manifest.stylesheet,
|
|
359
359
|
csp: config.csp,
|
|
360
360
|
lang: config.lang,
|
|
361
|
+
canonical: config.canonical,
|
|
361
362
|
// Written by the build and carried here, so a server-rendered
|
|
362
363
|
// page says the same thing about speculation that a file does.
|
|
363
364
|
speculate: manifest.speculate ?? null,
|
|
@@ -429,6 +430,7 @@ export function createApp({
|
|
|
429
430
|
stylesheet: manifest.stylesheet,
|
|
430
431
|
csp: config.csp,
|
|
431
432
|
lang: config.lang,
|
|
433
|
+
canonical: config.canonical,
|
|
432
434
|
speculate: manifest.speculate ?? null,
|
|
433
435
|
include,
|
|
434
436
|
});
|
package/src/compiler/bind.js
CHANGED
|
@@ -69,6 +69,7 @@ class Bindgen {
|
|
|
69
69
|
components = new Map(),
|
|
70
70
|
shadowTags = new Set(),
|
|
71
71
|
blockOf = new Map(),
|
|
72
|
+
anchoredOf = new Set(),
|
|
72
73
|
refs = new Map(),
|
|
73
74
|
} = {}) {
|
|
74
75
|
this.components = components;
|
|
@@ -76,6 +77,10 @@ class Bindgen {
|
|
|
76
77
|
// Which tree node owns which compiled block. Sharing the map is what keeps
|
|
77
78
|
// this pass and the renderer from drifting apart over the same tree.
|
|
78
79
|
this.blockOf = blockOf;
|
|
80
|
+
// The blocks the renderer fenced in anchors, which is more of them than it
|
|
81
|
+
// compiled. Shared for the same reason, and read for one question: is this a
|
|
82
|
+
// block the walk can step over.
|
|
83
|
+
this.anchoredOf = anchoredOf;
|
|
79
84
|
// tag -> the local name the renderer imported that component under.
|
|
80
85
|
this.refs = refs;
|
|
81
86
|
this.frames = [new Frame(new Scope())];
|
|
@@ -165,6 +170,11 @@ class Bindgen {
|
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
|
|
173
|
+
/** A `<slot>` the renderer fenced, which is a light element's and never a shadow one's. */
|
|
174
|
+
isHole(node) {
|
|
175
|
+
return node.tagName === 'slot' && this.anchoredOf.has(node);
|
|
176
|
+
}
|
|
177
|
+
|
|
168
178
|
abandon(slot) {
|
|
169
179
|
if (slot.kind === 'text') {
|
|
170
180
|
for (const node of slot.nodes) this.giveUpText(node.value);
|
|
@@ -208,14 +218,36 @@ class Bindgen {
|
|
|
208
218
|
if (slot.kind === 'block') {
|
|
209
219
|
const ref = this.bindBlock(slot, here);
|
|
210
220
|
if (ref === null) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
221
|
+
// A light element's block: rendered once and never rebuilt. What it
|
|
222
|
+
// read is volatile and its nodes are nothing to hold, but its anchors
|
|
223
|
+
// are in the markup, so the walk goes on past it. A block without them
|
|
224
|
+
// has no past it, and nothing after it binds.
|
|
225
|
+
if (!this.anchoredOf.has(slot.nodes[0])) {
|
|
226
|
+
for (const rest of rendered.slice(i)) this.abandon(rest);
|
|
227
|
+
this.frame.gaveUp = true;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.abandon(slot);
|
|
214
231
|
}
|
|
215
232
|
// Past a block the node count is not knowable, so addressing becomes
|
|
216
233
|
// relative from here on.
|
|
217
234
|
cursor = cursor ?? this.cursor();
|
|
218
|
-
|
|
235
|
+
const past = ref === null ? `__afterBlock(${here})` : `__b[${ref}].end.nextSibling`;
|
|
236
|
+
this.locate(`${cursor} = ${past};`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// A light element's `<slot>` is a compile-time hole holding the caller's
|
|
241
|
+
// markup, which is fenced for the same reason a block is. Nothing in it is
|
|
242
|
+
// ours to write, and the walk steps over it to reach what follows.
|
|
243
|
+
//
|
|
244
|
+
// The tag is asked for, not just the set: a branch arrives at its own part
|
|
245
|
+
// bare, with its directive already consumed, so a block's node reaches here
|
|
246
|
+
// as an ordinary element and is in the set too.
|
|
247
|
+
if (slot.kind === 'element' && this.isHole(slot.nodes[0])) {
|
|
248
|
+
this.abandon(slot);
|
|
249
|
+
cursor = cursor ?? this.cursor();
|
|
250
|
+
this.locate(`${cursor} = __afterBlock(${here});`);
|
|
219
251
|
continue;
|
|
220
252
|
}
|
|
221
253
|
|
package/src/compiler/codegen.js
CHANGED
|
@@ -98,6 +98,7 @@ export function compileFragment(nodes, opts = {}) {
|
|
|
98
98
|
at,
|
|
99
99
|
blockDefs: gen.blockDefs.join('\n'),
|
|
100
100
|
blockOf: gen.blockOf,
|
|
101
|
+
anchoredOf: gen.anchoredOf,
|
|
101
102
|
slots: Object.fromEntries(slots.map(([name, out]) => [name, out.code])),
|
|
102
103
|
regions: Object.fromEntries(regions.map(([name, out]) => [name, out.code])),
|
|
103
104
|
regionIncludes: gen.regionIncludes,
|
|
@@ -129,13 +130,16 @@ class Codegen {
|
|
|
129
130
|
// `blocks` on, an `if` or `each` compiles to its own module-scope function,
|
|
130
131
|
// and a `__fragment` passed from render would not be in scope inside it.
|
|
131
132
|
this.fragments = fragments;
|
|
132
|
-
// With `blocks` on, `if` and `each`
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
this.blocks = blocks && !layout;
|
|
133
|
+
// With `blocks` on, every `if` and `each` is wrapped in comment anchors, so
|
|
134
|
+
// whatever binds this markup can find where one ends. Only an element is ever
|
|
135
|
+
// updated, so nothing else pays for them.
|
|
136
|
+
this.blocks = blocks;
|
|
137
137
|
this.blockDefs = [];
|
|
138
138
|
this.blockOf = new Map();
|
|
139
|
+
// Every block that got anchors, whether or not it also got a function. The
|
|
140
|
+
// binding pass reads it the way it reads `blockOf`: to know that a block it
|
|
141
|
+
// cannot bind is still one it can step over.
|
|
142
|
+
this.anchoredOf = new Set();
|
|
139
143
|
this.inBlock = 0;
|
|
140
144
|
// The loop variables in scope, outermost first, two per level. A block
|
|
141
145
|
// inside a loop renders from them, so its function has to take them.
|
|
@@ -360,14 +364,25 @@ class Codegen {
|
|
|
360
364
|
}
|
|
361
365
|
|
|
362
366
|
/**
|
|
363
|
-
* True where a
|
|
364
|
-
* the runtime counts depth,
|
|
365
|
-
*
|
|
367
|
+
* True where a block is wrapped in anchors, which is where anything binds this
|
|
368
|
+
* markup at all. Anchors nest and the runtime counts depth, so nesting is not a
|
|
369
|
+
* reason to give up.
|
|
366
370
|
*/
|
|
367
|
-
|
|
371
|
+
anchored() {
|
|
368
372
|
return this.blocks && this.inHead === 0;
|
|
369
373
|
}
|
|
370
374
|
|
|
375
|
+
/**
|
|
376
|
+
* True where a block also compiles to a function of its own, which is what
|
|
377
|
+
* re-rendering one region rather than the whole root needs. Not in a layout,
|
|
378
|
+
* and a light element is compiled as one: `<slot>` there is a compile-time hole
|
|
379
|
+
* reading `__slots`, a parameter of `render` that a module-scope block function
|
|
380
|
+
* would not have. A light element rebuilds nothing anyway.
|
|
381
|
+
*/
|
|
382
|
+
standalone() {
|
|
383
|
+
return this.anchored() && !this.layout;
|
|
384
|
+
}
|
|
385
|
+
|
|
371
386
|
/** Flat list of the loop variables in scope, outermost first. */
|
|
372
387
|
loopArgs() {
|
|
373
388
|
return this.loops.flatMap((loop) => [loop.item, loop.index]);
|
|
@@ -382,6 +397,7 @@ class Codegen {
|
|
|
382
397
|
const id = this.blockDefs.length;
|
|
383
398
|
const params = ['__d', ...args].join(', ');
|
|
384
399
|
this.blockOf.set(node, id);
|
|
400
|
+
this.anchoredOf.add(node);
|
|
385
401
|
this.blockDefs.push(
|
|
386
402
|
`const __blk${id} = { ${extra}html: (${params}) => { let __o = '';\n${joinOut(body).code}\nreturn __o; } };`,
|
|
387
403
|
);
|
|
@@ -400,7 +416,16 @@ class Codegen {
|
|
|
400
416
|
this.emitBlock(chain[0].node, out, body, '', args);
|
|
401
417
|
return;
|
|
402
418
|
}
|
|
419
|
+
// Emitted where it stands, and still fenced. The markup is rendered once and
|
|
420
|
+
// nothing will replace it, but the anchors are how a walk gets to the nodes
|
|
421
|
+
// after it: what a branch renders is only known once the data is.
|
|
422
|
+
const fenced = this.anchored();
|
|
423
|
+
if (fenced) {
|
|
424
|
+
this.anchoredOf.add(chain[0].node);
|
|
425
|
+
this.s(out, ANCHOR_OPEN);
|
|
426
|
+
}
|
|
403
427
|
this.emitBranches(chain, out, scope, topLevel);
|
|
428
|
+
if (fenced) this.s(out, ANCHOR_CLOSE);
|
|
404
429
|
}
|
|
405
430
|
|
|
406
431
|
emitBranches(chain, out, scope, topLevel) {
|
|
@@ -537,6 +562,7 @@ class Codegen {
|
|
|
537
562
|
const id = this.blockDefs.length;
|
|
538
563
|
this.blockDefs.push('');
|
|
539
564
|
this.blockOf.set(el, id);
|
|
565
|
+
this.anchoredOf.add(el);
|
|
540
566
|
|
|
541
567
|
// A <template each> renders several nodes per item, so an item is a
|
|
542
568
|
// region rather than a node and needs anchors of its own to be found.
|
|
@@ -562,7 +588,15 @@ class Codegen {
|
|
|
562
588
|
return;
|
|
563
589
|
}
|
|
564
590
|
|
|
591
|
+
// The same fence an inline `if` gets, and for the same reason: how many nodes
|
|
592
|
+
// the loop produces is a question only the data answers.
|
|
593
|
+
const fenced = this.anchored();
|
|
594
|
+
if (fenced) {
|
|
595
|
+
this.anchoredOf.add(el);
|
|
596
|
+
this.s(out, ANCHOR_OPEN);
|
|
597
|
+
}
|
|
565
598
|
this.emitEachBody(el, out, scope, topLevel);
|
|
599
|
+
if (fenced) this.s(out, ANCHOR_CLOSE);
|
|
566
600
|
}
|
|
567
601
|
|
|
568
602
|
/** `list`, `key` and `item`. One loop, taken apart so it can be reconciled. */
|
|
@@ -653,13 +687,24 @@ class Codegen {
|
|
|
653
687
|
const filled = `__slots[${JSON.stringify(name)}]`;
|
|
654
688
|
const fallback = childrenOf(el);
|
|
655
689
|
|
|
656
|
-
|
|
690
|
+
// The caller's markup, and how many nodes it is only this render knows. So
|
|
691
|
+
// it is fenced like a block, and for the same reason: nothing that binds
|
|
692
|
+
// this markup afterwards can count its way past it.
|
|
693
|
+
const fenced = this.anchored();
|
|
694
|
+
if (fenced) {
|
|
695
|
+
this.anchoredOf.add(el);
|
|
696
|
+
this.s(out, ANCHOR_OPEN);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (fallback.length) {
|
|
700
|
+
this.c(out, `if (${filled}) { __o += ${filled}; } else {`);
|
|
701
|
+
this.emitChildren(fallback, out, scope);
|
|
702
|
+
this.c(out, `}`);
|
|
703
|
+
} else {
|
|
657
704
|
this.c(out, `__o += ${filled} ?? '';`);
|
|
658
|
-
return;
|
|
659
705
|
}
|
|
660
|
-
|
|
661
|
-
this.
|
|
662
|
-
this.c(out, `}`);
|
|
706
|
+
|
|
707
|
+
if (fenced) this.s(out, ANCHOR_CLOSE);
|
|
663
708
|
return;
|
|
664
709
|
}
|
|
665
710
|
|
package/src/compiler/index.js
CHANGED
|
@@ -279,6 +279,7 @@ export function compileComponent(
|
|
|
279
279
|
components,
|
|
280
280
|
shadowTags,
|
|
281
281
|
blockOf: template.blockOf,
|
|
282
|
+
anchoredOf: template.anchoredOf,
|
|
282
283
|
refs: new Map(template.components.map(({ tag: name, ref }) => [name, ref])),
|
|
283
284
|
// The runtime prepends <style> to the shadow root, so a component's own
|
|
284
285
|
// first node is not at index 0. A light element's styles are hoisted
|
|
@@ -851,7 +852,7 @@ function unusedProps(defaultNode, reads, blocks) {
|
|
|
851
852
|
// ---- module assembly helpers ---------------------------------------------
|
|
852
853
|
|
|
853
854
|
function runtimeImport(runtime) {
|
|
854
|
-
return `import { escape as __e, attr as __a, attrProp as __ap, str as __str, json, shadow as __sh, data as __data, included as __incl, textAt as __textAt, setText as __setText, setParts as __setParts, setAttr as __setAttr, setAttrProp as __setAttrProp, blockAt as __blockAt, updateBlock as __updateBlock, coerceProps, defineComponent, defineLight, html } from ${JSON.stringify(runtime)};`;
|
|
855
|
+
return `import { escape as __e, attr as __a, attrProp as __ap, str as __str, json, shadow as __sh, data as __data, included as __incl, textAt as __textAt, setText as __setText, setParts as __setParts, setAttr as __setAttr, setAttrProp as __setAttrProp, blockAt as __blockAt, afterBlock as __afterBlock, updateBlock as __updateBlock, coerceProps, defineComponent, defineLight, html } from ${JSON.stringify(runtime)};`;
|
|
855
856
|
}
|
|
856
857
|
|
|
857
858
|
function layoutImports(layouts) {
|
package/src/defaults.js
CHANGED
|
@@ -34,6 +34,11 @@ export const DEFAULTS = {
|
|
|
34
34
|
csrf: true,
|
|
35
35
|
csp: false,
|
|
36
36
|
speculate: false,
|
|
37
|
+
// `<link rel="canonical">` on every page, pointing at the page's own URL. Off
|
|
38
|
+
// by default because a page mounted at a second URL on purpose would get a
|
|
39
|
+
// wrong one, and a wrong canonical is worse than none: it hands the ranking to
|
|
40
|
+
// the other URL.
|
|
41
|
+
canonical: false,
|
|
37
42
|
// `(source, file) => html`, and a `.md` page under `routes/` without one is an
|
|
38
43
|
// error naming the file. This package ships no Markdown parser: which flavor
|
|
39
44
|
// and which extensions are the app's to pick, the same way `cache` is a store
|
|
@@ -41,6 +46,31 @@ export const DEFAULTS = {
|
|
|
41
46
|
markdown: null,
|
|
42
47
|
};
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Keys a config may set that have no default, listed so the check below does not
|
|
51
|
+
* read them as typos.
|
|
52
|
+
*
|
|
53
|
+
* A key is here when leaving it out has to mean something other than a value.
|
|
54
|
+
* There is no feed to write down as the default feed, and no store to write down
|
|
55
|
+
* as the default `cache`: absent is how an app says it wants neither.
|
|
56
|
+
*/
|
|
57
|
+
const UNDEFAULTED = [
|
|
58
|
+
'cache',
|
|
59
|
+
'cookieSecret',
|
|
60
|
+
'feed',
|
|
61
|
+
'fragmentHeader',
|
|
62
|
+
'metadataBase',
|
|
63
|
+
'onError',
|
|
64
|
+
'port',
|
|
65
|
+
'precache',
|
|
66
|
+
'proxy',
|
|
67
|
+
'sitemap',
|
|
68
|
+
'watchElements',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** Every key `transclude.config.js` may set. */
|
|
72
|
+
export const KEYS = new Set([...Object.keys(DEFAULTS), ...UNDEFAULTED]);
|
|
73
|
+
|
|
44
74
|
/**
|
|
45
75
|
* A config with every default filled in.
|
|
46
76
|
*
|
|
@@ -50,7 +80,34 @@ export const DEFAULTS = {
|
|
|
50
80
|
*
|
|
51
81
|
* @param {object} [config] whatever `transclude.config.js` exported
|
|
52
82
|
* @returns {object} the same keys, plus the ones it did not mention
|
|
83
|
+
* @throws when `canonical` is on and there is no origin to build a URL from
|
|
53
84
|
*/
|
|
54
85
|
export function withDefaults(config = {}) {
|
|
55
|
-
|
|
86
|
+
// A key nothing reads is a line the author believes is doing something. The
|
|
87
|
+
// failure it replaces is silent and expensive: `stylesheeet` cost a site its
|
|
88
|
+
// whole stylesheet and said nothing, because an ignored key looks exactly like
|
|
89
|
+
// a key that worked.
|
|
90
|
+
const unknown = Object.keys(config).filter((key) => !KEYS.has(key));
|
|
91
|
+
if (unknown.length) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[transclude] transclude.config.js sets ${unknown.join(', ')}, which nothing reads. ` +
|
|
94
|
+
`The keys are ${[...KEYS].sort().join(', ')}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const merged = { ...DEFAULTS, ...config };
|
|
99
|
+
|
|
100
|
+
// Refused here because there are four places that render a page and only two of
|
|
101
|
+
// them could fall back to a request's origin. Left to the render, `canonical`
|
|
102
|
+
// would work in dev and throw in the build, which is the dev-and-production
|
|
103
|
+
// disagreement this file exists to stop.
|
|
104
|
+
if (merged.canonical && !merged.metadataBase) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`[transclude] \`canonical: true\` needs \`metadataBase\`, which is the origin the ` +
|
|
107
|
+
`URL is built from. A request's own origin is the wrong one twice: behind a proxy ` +
|
|
108
|
+
`it is the internal address, and a prerendered page has no request at all.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return merged;
|
|
56
113
|
}
|
package/src/document.js
CHANGED
|
@@ -177,6 +177,23 @@ const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
|
177
177
|
|
|
178
178
|
const escapeAttr = (value) => String(value).replace(/[&<>"]/g, (c) => ESCAPES[c]);
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* The head the framework writes, as the outermost level of the merge.
|
|
182
|
+
*
|
|
183
|
+
* Through `mergeHead` rather than beside it, so a page or a layout writing its
|
|
184
|
+
* own `viewport` or `canonical` replaces this one instead of shipping a second
|
|
185
|
+
* copy. `charset` is not here: it has to be inside the first 1024 bytes, and it
|
|
186
|
+
* is not something to override.
|
|
187
|
+
*
|
|
188
|
+
* @param {string|null} canonical the page's own URL, absolute, or null for none
|
|
189
|
+
* @returns {string} one level's worth of head markup
|
|
190
|
+
*/
|
|
191
|
+
function frameworkHead(canonical) {
|
|
192
|
+
const tags = ['<meta name="viewport" content="width=device-width, initial-scale=1">'];
|
|
193
|
+
if (canonical) tags.push(`<link rel="canonical" href="${escapeAttr(canonical)}">`);
|
|
194
|
+
return tags.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
180
197
|
/**
|
|
181
198
|
* `<html …>`, with `lang` first and whatever a loader added after it.
|
|
182
199
|
*
|
|
@@ -239,7 +256,8 @@ function openTag(tag, attrs) {
|
|
|
239
256
|
*
|
|
240
257
|
* @param {object} page a compiled page module
|
|
241
258
|
* @param {object} ctx the request context
|
|
242
|
-
* @param {object} [options] `clientEntry`, `stylesheet`, `csp`, `lang`, `include
|
|
259
|
+
* @param {object} [options] `clientEntry`, `stylesheet`, `csp`, `lang`, `include`,
|
|
260
|
+
* `canonical`
|
|
243
261
|
* @returns {Promise<string|Response>} a Response when a loader answered for itself
|
|
244
262
|
*/
|
|
245
263
|
export async function renderRoute(page, ctx, options = {}) {
|
|
@@ -266,7 +284,18 @@ export async function renderRoute(page, ctx, options = {}) {
|
|
|
266
284
|
if (mod !== page) inherited = { ...inherited, ...data };
|
|
267
285
|
}
|
|
268
286
|
|
|
269
|
-
|
|
287
|
+
// The config's `canonical` is a yes or no; the document's is the URL. Turned
|
|
288
|
+
// into one here because this is the layer that holds the request, and because
|
|
289
|
+
// the four callers that render a page would otherwise each compute it.
|
|
290
|
+
//
|
|
291
|
+
// `route.path` and not the request's URL: a canonical URL names the page, so a
|
|
292
|
+
// query parameter has no place in it. That the path is already free of a
|
|
293
|
+
// trailing slash is Hono's doing under `trailingSlash: 'ignore'`, which is the
|
|
294
|
+
// setting that makes this option worth having.
|
|
295
|
+
const html = renderDocument(chain, datas, {
|
|
296
|
+
...options,
|
|
297
|
+
canonical: options.canonical ? ctx.absolute(ctx.route.path) : null,
|
|
298
|
+
});
|
|
270
299
|
|
|
271
300
|
// After the document exists, because the policy is built from what it inlined.
|
|
272
301
|
// A prerendered page runs this once at build time and carries the result.
|
|
@@ -598,13 +627,15 @@ export function methodsOf(page) {
|
|
|
598
627
|
* @param {object[]} chain the compiled modules, outermost first
|
|
599
628
|
* @param {object[]} datas one per level, in the same order
|
|
600
629
|
* @param {{ clientEntry?: string|null, stylesheet?: string|null, lang?: string,
|
|
601
|
-
* speculate?: string|null }} [options]
|
|
630
|
+
* speculate?: string|null, canonical?: string|null }} [options] `canonical` is
|
|
631
|
+
* the URL itself, already absolute. `renderRoute` is what turns the config's
|
|
632
|
+
* yes-or-no into one, because this function sees no request.
|
|
602
633
|
* @returns {string} the document, starting at `<!doctype html>`
|
|
603
634
|
*/
|
|
604
635
|
export function renderDocument(
|
|
605
636
|
chain,
|
|
606
637
|
datas,
|
|
607
|
-
{ clientEntry, stylesheet, lang = 'en', speculate = null } = {},
|
|
638
|
+
{ clientEntry, stylesheet, lang = 'en', speculate = null, canonical = null } = {},
|
|
608
639
|
) {
|
|
609
640
|
// Each level renders to a slot map and hands it to the level above, so a page
|
|
610
641
|
// can fill more than one hole in its layout.
|
|
@@ -624,13 +655,10 @@ export function renderDocument(
|
|
|
624
655
|
}
|
|
625
656
|
|
|
626
657
|
// Everything else accumulates outermost first, so a page's <meta> comes last
|
|
627
|
-
// and a page's <style> can override a layout's.
|
|
628
|
-
//
|
|
629
|
-
// a page or a layout writing its own `viewport` replaces this one instead of
|
|
630
|
-
// shipping beside it. `charset` is not here: it has to be inside the first
|
|
631
|
-
// 1024 bytes and is not something to override.
|
|
658
|
+
// and a page's <style> can override a layout's. The framework's own head is the
|
|
659
|
+
// outermost level; `frameworkHead` says what is in it and why.
|
|
632
660
|
const [defaults, ...rest] = mergeHead([
|
|
633
|
-
|
|
661
|
+
frameworkHead(canonical),
|
|
634
662
|
...chain.map((mod, i) => mod.renderHead(datas[i])),
|
|
635
663
|
]);
|
|
636
664
|
const head = rest.filter(Boolean);
|
package/src/runtime/index.js
CHANGED
|
@@ -308,6 +308,20 @@ function closingAnchor(open) {
|
|
|
308
308
|
return null;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The node after a block nobody bound.
|
|
313
|
+
*
|
|
314
|
+
* A light element renders its blocks once and never rebuilds them, so it holds
|
|
315
|
+
* no state for one. The walk that finds every node after it still has to get
|
|
316
|
+
* past it, and how wide it is only the anchors say.
|
|
317
|
+
*
|
|
318
|
+
* @param {Comment} open the opening anchor
|
|
319
|
+
* @returns {Node|null}
|
|
320
|
+
*/
|
|
321
|
+
export function afterBlock(open) {
|
|
322
|
+
return closingAnchor(open)?.nextSibling ?? null;
|
|
323
|
+
}
|
|
324
|
+
|
|
311
325
|
/**
|
|
312
326
|
* An item spans one node or, where it renders several, the region between its
|
|
313
327
|
* own anchors. Everything downstream works on the range, so a single-element
|
package/src/server.js
CHANGED
|
@@ -69,7 +69,9 @@ export function baseApp(options = {}) {
|
|
|
69
69
|
* So 'never' means strict routing plus a 301 to the one URL, and every
|
|
70
70
|
* URL this framework generates is already that form: `routes/about.html` is
|
|
71
71
|
* `/about`. 'ignore' is the loose router, which answers both with 200. Two URLs
|
|
72
|
-
* for one page,
|
|
72
|
+
* for one page, which is what `canonical: true` in the config answers: the
|
|
73
|
+
* loose router hands `c.req.path` over with the slash already gone, so the URL
|
|
74
|
+
* the tag names is the one form either way.
|
|
73
75
|
*
|
|
74
76
|
* `alwaysRedirect` matters because Hono's default only redirects a request that
|
|
75
77
|
* already 404'd, and a catch-all route answers before it can. `/docs/intro/`
|