vike icon indicating copy to clipboard operation
vike copied to clipboard

New hook `onRenderHtmlEnd()`

Open brillout opened this issue 1 year ago • 11 comments

Description

The onAfterRenderHtml() hook is called before Vike injects assets, which works for most use cases. But it doesn't work for some use cases.

Proposal: new hook onRenderHtmlEnd() called after the HTML is fully generated (after Vike injects assets).

Use case: HTML transformers, such as critical CSS inlining https://github.com/vikejs/vike/discussions/1975.

Workaround in the meantime:

  • SSG: crawl HTML files in dist/client/ and apply transformer against each HTML file.
  • SSR: run transformer after renderPage().

(Note to maintainers: ideally the name should be swapped with the already existing onAfterRenderHtml().)

brillout avatar Nov 22 '24 17:11 brillout

Need this feature, I tried to use unhead render head (I am trying to migrate to vike, and I haven’t used +Head yet in order to minimize code modifications.), but found that the onAfterRenderHtml method only has the HTML inside #app. onAfterRenderHtml() looks like it should be called after onRenderHtml(), but it is actually called at the beginning of onRenderHtml(). It should be called onAfterRenderPageToHtml()

Ercilan avatar Aug 31 '25 07:08 Ercilan

@Ercilan Have you seen https://vike.dev/onAfterRenderHtml#pagecontext-pagehtml-string-stream ?

only has the HTML inside #app.

No, it does have the entire HTML. (Minus the <script> and <style> tags injected by Vike.)

brillout avatar Aug 31 '25 09:08 brillout

only has the HTML inside #app.

No, it does have the entire HTML. (Minus the <script> and <style> tags injected by Vike.)

@brillout You can see this comment in vike-vue directly:

pageContext.{pageHtmlString,pageHtmlStream} is set by renderPageToHtml()

https://github.com/vikejs/vike-vue/blob/0eeb2ecd62fe26674a8502d22d5d2c7ef5065924/packages/vike-vue/src/integration/onRenderHtml.ts#L34-L67

onRenderHtml() returns complete HTML, but onAfterRenderHtml() does not.

// from
pageHtmlStringOrStream = pageContext.pageHtmlString or pageContext.pageHtmlStream

// return
<div id="app">${pageHtmlStringOrStream}</div>

Ercilan avatar Aug 31 '25 09:08 Ercilan

Ah, you're right, you can't access headHtml. How about exposing it over pageContext.headHtml? Would that solve your issue?

brillout avatar Aug 31 '25 10:08 brillout

For my own use case, this might work, but for the full functionality of unhead, it’s not enough — for example, it supports modifying htmlAttrs to set the language. It needs the entire HTML.

This is what unhead will do:

export async function transformHtmlTemplate(head: Unhead<any>, html: string, options?: RenderSSRHeadOptions) {
  const { html: parsedHtml, input } = extractUnheadInputFromHtml(html)
  head.push(input, { _index: 0 })
  const headHtml = await renderSSRHead(head, options)
  return parsedHtml
    .replace('<html>', `<html${headHtml.htmlAttrs}>`)
    .replace('<body>', `<body>${headHtml.bodyTagsOpen ? `\n${headHtml.bodyTagsOpen}` : ``}`)
    .replace('<body>', `<body${headHtml.bodyAttrs}>`)
    .replace('</head>', `${headHtml.headTags}</head>`)
    .replace('</body>', `${headHtml.bodyTags}</body>`)
}

I think it’s better to provide the full HTML. That way it matches the intuitive meaning of the function name AfterRenderHtml and also makes it easier to handle (not only for unhead but also for other potential use cases).

Ercilan avatar Aug 31 '25 11:08 Ercilan

@Ercilan Can you use the workaround mentioned here?

I think it’s better to provide the full HTML. That way it matches the intuitive meaning of the function name AfterRenderHtml and also makes it easier to handle (not only for unhead but also for other potential use cases).

I agree and I would implement this now, but it's a breaking change so I'm inclined to work on this only after more users want it (or if it's a blocker for someone which doesn't seem to be the case since there is a workaround).

brillout avatar Sep 01 '25 13:09 brillout

@brillout I try SSG, but unhead requires using pageContext which stores the configuration that unhead reads from each page(i add it to pageContext). If I write a Vite plugin myself to handle this, I won’t be able to access it, right?

Ercilan avatar Sep 02 '25 15:09 Ercilan

https://github.com/magne4000/vite-plugin-vercel/blob/14ffa67357eb27ee425829defdaf1efda3cbde39/packages/vike/src/plugins/prerender.ts#L40-L41

type PrerenderContext = {
  pageContexts: PageContext[]
  output: Output
  _pageContextInit: Record<string, unknown> | null
  _noExtraDir: boolean | null
  _prerenderedPageContexts: PrerenderedPageContexts
}

brillout avatar Sep 02 '25 21:09 brillout

@brillout It seems that if I’m not doing full SSG, then I get:

prerenderContext: {
  isPrerenderingEnabled: true,
  isPrerenderingEnabledForAllPages: false,
  output: null,
  pageContexts: null
}

right? Also, I have another question: if I plan to use mostly SSG + some SPA, do I still need a Node.js server? From what I see, in this case it still requires a server folder in order to run preview.

Ercilan avatar Sep 03 '25 15:09 Ercilan

if I plan to use mostly SSG + some SPA, do I still need a Node.js server?

https://github.com/vikejs/vike/commit/108f4b527e91b19c03cb217299bc0b0a73a5e265 https://vike.dev/pre-rendering#ssg-vs-ssr-vs-spa

brillout avatar Sep 04 '25 12:09 brillout

https://github.com/vikejs/vike/pull/2692 https://vike.dev/globalContext#prerenderContext https://vike.dev/api#prerender https://vike.dev/api#access-globalcontext

brillout avatar Sep 04 '25 15:09 brillout