@octanejs/app-core 0.0.12 → 0.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/app-core",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -79,10 +79,10 @@
79
79
  "esbuild": "^0.28.1"
80
80
  },
81
81
  "peerDependencies": {
82
- "octane": "0.1.16"
82
+ "octane": "0.1.17"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^24.13.3",
86
- "octane": "0.1.16"
86
+ "octane": "0.1.17"
87
87
  }
88
88
  }
@@ -279,39 +279,67 @@ export function createHandler(manifest, deps) {
279
279
  }
280
280
 
281
281
  const headContent = [...preloadTags, dataScript].join('\n');
282
- const html = applyHydrationNonce(htmlTemplate, nonce).replace('<!--ssr-head-->', headContent);
282
+ const noncedTemplate = applyHydrationNonce(htmlTemplate, nonce);
283
283
 
284
284
  const status = route.status ?? 200;
285
285
  const headers = { 'Content-Type': 'text/html; charset=utf-8' };
286
286
 
287
- const [prefix, suffix] = splitSsrTemplate(html);
287
+ // `headChannel: 'separate'` below: this handler renders the ROUTE into the
288
+ // template's `<div id="root">`, not a document, so core's default fold has
289
+ // no `</head>` to target and would prepend hoisted `<title>`/`<meta>`/
290
+ // `<link>` into the body, where a title loses to the template's and a
291
+ // canonical or description is ignored. Splicing at `<!--ssr-head-->`
292
+ // instead is also what makes hydration's `document.head` adoption find the
293
+ // ownership markers rather than creating duplicates.
294
+ //
295
+ // The replacement is a FUNCTION, not a string: a string replacement makes
296
+ // `$&`, `` $` ``, `$'` and `$1` in the inserted text expand against the
297
+ // match, and this text now carries author-controlled metadata as well as
298
+ // the serialized route data.
299
+ /** @param {string} hoistedHead */
300
+ const splitAroundBody = (hoistedHead) =>
301
+ splitSsrTemplate(noncedTemplate.replace('<!--ssr-head-->', () => headContent + hoistedHead));
288
302
 
289
303
  if (manifest.render === 'buffered') {
290
304
  // Await-everything fallback (`prerender` from octane/static): no
291
305
  // streaming, one document. The deduped scoped-style tags lead the body
292
306
  // markup inside #root — the same position they hold in the streamed
293
307
  // shell — so hydrateRoot's leading-style skip applies unchanged.
294
- const { html: body, css } = await prerender(RootComponent, undefined, {
308
+ const {
309
+ html: body,
310
+ css,
311
+ head,
312
+ } = await prerender(RootComponent, undefined, {
295
313
  nonce: nonce ?? undefined,
314
+ headChannel: 'separate',
296
315
  signal: context.request.signal,
297
316
  onError(/** @type {unknown} */ error) {
298
317
  console.error('[octane] SSR render error:', error);
299
318
  },
300
319
  });
320
+ const [prefix, suffix] = splitAroundBody(head ?? '');
301
321
  return new Response(prefix + css + body + suffix, { status, headers });
302
322
  }
303
323
 
304
324
  // Streaming (default): shell flushes at first await, suspense segments
305
325
  // stream out-of-order behind it — identical to dev.
326
+ let hoistedHead = '';
306
327
  /** @type {ReadableStream<Uint8Array>} */
307
328
  const renderStream = await renderToReadableStream(RootComponent, undefined, {
308
329
  nonce: nonce ?? undefined,
330
+ headChannel: 'separate',
331
+ // Fires before the shell is written, so the metadata is in hand before
332
+ // the template prefix (which carries `<head>`) is composed below.
333
+ onHeadReady(/** @type {string} */ head) {
334
+ hoistedHead = head;
335
+ },
309
336
  signal: context.request.signal,
310
337
  onError(/** @type {unknown} */ error) {
311
338
  console.error('[octane] SSR render error:', error);
312
339
  },
313
340
  });
314
341
 
342
+ const [prefix, suffix] = splitAroundBody(hoistedHead);
315
343
  const body = composeHtmlStream(prefix, renderStream, suffix);
316
344
 
317
345
  return new Response(body, { status, headers });