How to use SSR with Stencil in a Nuxt 3 Vite project?

2.1k Views Asked by At

In Nuxt 2 I could use server-side rendered Stencil components by leveraging the renderToString() method provided in the Stencil package in combination with a Nuxt hook, like this:

import { renderToString } from '[my-components]/dist-hydrate'

export default function () {
  this.nuxt.hook('generate:page', async (page) => {
    const render = await renderToString(page.html, {
      prettyHtml: false
    })
    page.html = render.html
  })
}

Since the recent release of Stencil 2.16.0 I'm able to use native web components in Nuxt 3 that is powered by Vite. However I haven't found a way to hook into the template hydration process. Unfortunately there is no documentation for the composable useHydration() yet.

Does anybody know how I could get this to work in Nuxt 3?

4

There are 4 best solutions below

0
On

In addition the solution for the prerendered content in the solution by John Jenkins.
To keep JS functionality on the client side, one thing that helped me was a client plugin to render the component, but directly from the component, not the loader because the loader was giving me some troubles with the chunks ids

example:

./plugins/example-plugin.client.js

import { defineNuxtPlugin } from '#app'
import { defineCustomElement } from '@yourComponentLibrary/dist/components/yourCompoent';

export default defineNuxtPlugin(async () => {
  await defineCustomElement(window)
})
0
On

I've found defining a plugin using the 'render:response' hook to work for me:

server/plugins/ssr-components.plugin.ts

import { renderToString } from '@my-lib/components/hydrate';

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('render:response', async (response) => {
    response.body = (await renderToString(response.body)).html;
  });
});

Perhaps it will work for you :)

0
On

Try this in defineNuxtPlugin

  nuxtApp.hook('app:rendered', () => {
    const response = nuxtApp.ssrContext?.res
    if (!response)
      return
    const end = response.end
    response.end = function(chunk) {
      chunk = 'hijacked'
      end(chunk)
    }
  })
4
On

I had the same problem. I solved it via a module.

  1. Make a new custom nuxt module. documentation for creating a module

  2. In the setup method hook into the generate:page hook:

    nuxt.hook('generate:page', async (page) => {
      const render = await renderToString(page.html, {
        prettyHtml: true,
      });
      page.html = render.html;
    });
    

    documentation for nuxt hooks

    documentation for stencil hydration (renderToString)

  3. Register the css classes you need via nuxt.options.css.push(PATH_TO_CSS)

  4. Register the module in the nuxt config.

    Note: Make sure in the nuxt.config.ts the defineNuxtConfig gets exported as default.

  5. Tap the vue compiler options in the nuxt config:

    vue: {
      compilerOptions: {
        isCustomElement: (tag) => TEST_TAG_HERE,
      },
    },
    
  6. This depends on how you wan't to use the custom elements. In my case I defined the elements over the stencil loader in my app.vue file:

    import { defineCustomElements } from '<package>/<path_to_loader>';
    defineCustomElements();
    

    You could also import the elements you need in your component and then define them right there, for example in a example.vue component:

    import { CustomElement } from '<package>/custom-elements';
    customElements.define('custom-element', CustomElement);
    

Here is an example from my module and config:

./modules/sdx.ts
import { defineNuxtModule } from '@nuxt/kit';
import { renderToString } from '@swisscom/sdx/hydrate';

export default defineNuxtModule({
  meta: {
    name: '@nuxt/sdx',
    configKey: 'sdx',
  },
  setup(options, nuxt) {
    nuxt.hook('generate:page', async (page) => {
      const render = await renderToString(page.html, {
        prettyHtml: true,
      });
      page.html = render.html;
    });
    nuxt.options.css.push('@swisscom/sdx/dist/css/webcomponents.css');
    nuxt.options.css.push('@swisscom/sdx/dist/css/sdx.css');
  },
});

Important: This only works if the stenciljs package supports hydration or in other words has a hydrate output. Read more here

./nuxt.config.ts
import { defineNuxtConfig } from 'nuxt';

//v3.nuxtjs.org/api/configuration/nuxt.config export default
export default defineNuxtConfig({
  typescript: { shim: false },
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => /sdx-.+/.test(tag),
    },
  },
  modules: ['./modules/sdx'],
});

./app.vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<script setup lang="ts">
import { defineCustomElements } from '@swisscom/sdx/dist/js/webcomponents/loader';

defineCustomElements();

// https://v3.nuxtjs.org/guide/features/head-management/
useHead({
  title: 'demo',
  viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
  charset: 'utf-8',
  meta: [{ name: 'description', content: 'demo for using a stencil package in a nuxt ssr app' }],
  bodyAttrs: {
    class: 'sdx',
  },
});
</script>

Update

I tested my setup with multiple components and it looks like you cannot define your components in the module. I updated the answer to my working solution.