vue-dompurify-html icon indicating copy to clipboard operation
vue-dompurify-html copied to clipboard

Contents not rendered at server-side with nuxt.

Open maninak opened this issue 2 years ago • 23 comments

Hello,

thanks for the great plugin!

It seems that when doing static site generation (SSG) on the server-side (I suspect it also applies for the SSR use case), any HTML injected into the dom via dompurify-html will not be present in the pre-rendered HTML.

Sure, the content will be added to the DOM after the initial page visit at hydration time, but that causes multiple layout shifts as content come into the page increasing our CLS performance metric massively, not only offering a worse experience to the users but also affecting our SEO ranking. Another (possible) SEO hit comes more directly because the original HTML is missing crucial content.

FYI I've already seen this closed MR https://github.com/LeSuisse/vue-dompurify-html/pull/591

Tested with vue-dompurify-html v2.5.0

maninak avatar May 19 '22 12:05 maninak

Hi,

Yes this is expected at this stage, this why the documentation ask to load it on the client side: https://github.com/LeSuisse/vue-dompurify-html/tree/vue-legacy#usage-with-nuxt

To make it work, it requires to initialize DOMPurify with JSDOM since there is no DOM to manipulate when running server side.

I will take a look to make possible to choose how DOMPurify is initialized so it is possible to get a DOMPurify instance with JSDOM and publish a proper Nuxt module to ease the installation/setup process.

That being said I'm not sure to understand the use case of DOMPurify/vue-dompurify-html in a SSG scenario. Do you not control all the inputs in this situation?

LeSuisse avatar May 19 '22 13:05 LeSuisse

Hey (and thanks for responding so quickly!),

here's a simplified way of how I'm using it in a vue component

<template>
  <article>
    <div dompurify-html="item.richtext"></div>
  </article>
</template>

and of course, following the docs, it's added on nuxt.config.js like so:

{
  //...
  plugins: [
    // ...
    { src: '~/plugins/vue-dompurify-html', mode: 'client' },
  ],
}

I'm not sure to understand the use case of DOMPurify/vue-dompurify-html in a SSG scenario. Do you not control all the inputs in this situation?

If I understand your question correctly, then yes, I control the input (here item.richtext) which contains rich text content in the form of HTML. Item is coming from a headless CMS BE (Strapi in this case). During nuxt build with static: true to enable SSG, the data for item will normally be fetched at build time and be used to pre-render the .html file for that page. But that div with the dompurify-html directive will be empty in the generated HTML causing the issues I described in my original post.

I think I'm doing everything the standard way. Please let me know if I should do things differently or if there's a way to fix my issue. Also, let me know if there's any more info that would be helpful.

maninak avatar May 19 '22 13:05 maninak

If I understand your question correctly, then yes, I control the input (here item.richtext) which contains rich text content in the form of HTML. Item is coming from a headless CMS BE (Strapi in this case). During nuxt build with static: true to enable SSG, the data for item will normally be fetched at build time and be used to pre-render the .html file for that page. But that div with the dompurify-html directive will be empty in the generated HTML causing the issues I described in my original post.

OK got it, it makes sense to me now. You are pulling untrusted data at build time.

I think I'm doing everything the standard way. Please let me know if I should do things differently or if there's a way to fix my issue. Also, let me know if there's any more info that would be helpful.

For now the only solution is to use it client side.

LeSuisse avatar May 19 '22 13:05 LeSuisse

Understood. Thanks for letting me know. I'll watch this issue in case the feature is added in the future.

It's worth sharing here, that many (most?) nuxt users fetch page data from a CMS, which very often contains HMTL (as rich text that non-technical CMS users author on a CMS word-like text editor) that needs to be injected into the page and be present at server-side generation.

And of course, given that injected HTML is expected to be everywhere for CMS-driven websites, sanitization on every page is a no-brainer.

So I'm impressed this issue hasn't come up before, because it sounds to me that my use case should be the main "target group" of this plugin. I could be wrong of course.

Thanks for taking the time to respond! :pray:

maninak avatar May 19 '22 14:05 maninak

Hi,

I did some changes to expose the necessary primitive so the directive can be also used on the server side. You can see the setup here: https://github.com/LeSuisse/vue-dompurify-html/tree/vue-legacy#server-side

I will take a look to provide a Nuxt module for the v3 to make the setup easier.

LeSuisse avatar Jun 05 '22 17:06 LeSuisse

Hi @LeSuisse, I'm trying to implement this solution with nuxt v2.15.8 and vue v2.6.14. As @maninak I'm using Strapi in my project and I'm using also richText components, so I'm pulling HTML at build time. I'm sorry but it's not clear to me how to implement the server-side directive. I'm not sure if I'm importing the function from the right module this is my implementation:

nuxt.config.js

import DOMPurify from 'dompurify'
import { buildVueDompurifyHTMLDirective } from 'vue-dompurify-html'
...
render: {
    bundleRenderer: {
      directives: {
        'dompurify-html': (el, dir) => {
          const insertHook = buildVueDompurifyHTMLDirective({}, () => {
            const window = new JSDOM('').window
            return DOMPurify(window)
          }).inserted
          insertHook(el, dir)
          el.data.domProps = { innerHTML: el.innerHTML }
        },
      },
    },
  }

BTW, I also had to extend nuxt's webpack to load the vue-dompurify-html mjs module due to this error:

[develop:frontend]  ERROR  in ./node_modules/vue-dompurify-html/dist/vue-dompurify-html.mjs
[develop:frontend]
[develop:frontend] Can't import the named export 'isVue3' from non EcmaScript module (only default export is available)

I extended webpack config with: (in nuxt.config.js)

 ...
  build: {
    extend(config, ctx) {
      config.module.rules.push({
        test: /\.mjs$/,
        include: /node_modules/,
        type: 'javascript/auto',
      })
    },
  },
...

I'm arrived to this issue looking for a solution of this error:

[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

image

This error doesn't happen if I don't use the $md.render(richText) within the v-dompurify-html directive.

Please, can you tell me what I'm doing wrong?

osroca avatar Aug 31 '22 18:08 osroca

The import issue are likely caused by the v3 since we are now also publishing ESM with the package.

That being said I'm a bit surprise you are using vue-dompurify-html v3 with Vue 2.6.14 https://github.com/LeSuisse/vue-dompurify-html/blob/7df8c4f2e29cf06ab166271037f10cb1cf14abf0/packages/vue-dompurify-html/package.json#L47 I would suggest to try with vue-dompurify-html 2.6.0 to see if you have the same issue.

Anyway something might be broken with Nuxt and Vue 2.7 with vue-dompurify-html v3. Personally I do not use Nuxt so I will check when I got some free time (likely end of this month, beginning of next one). Also, I still have the idea to publish a Nuxt module to ease the setup phase for Nuxt users.

LeSuisse avatar Sep 05 '22 11:09 LeSuisse

@LeSuisse thanks a lot for getting back to me. I'll try with 2.6.0 and I'll let you know. Please, let me know if having a repro repo would help you in the debug and I can share my project. It is a bit meshy because I'm starting with Stratpi but I hope it can help you.

osroca avatar Sep 05 '22 12:09 osroca

@osroca I made an example of using modules in nuxt2. hope this helps you. https://github.com/serialine/vue-dompurify-html/tree/example-nuxt2/examples/nuxt2 or #2257

serialine avatar Oct 20 '22 06:10 serialine

Thanks for the PR @serialine!

LeSuisse avatar Oct 20 '22 17:10 LeSuisse

@LeSuisse did you already try to get this to run in nuxt 3 when using SSR mode? It looks like the bundleRenderer is not as easily accessible anymore compared to Nuxt 2, though maybe I haven't found the right documentation, yet. I'll fiddle around in the next days, but I'd appreciate any information that you have. Thank you.

Pecral avatar Mar 06 '23 21:03 Pecral

No I did not but it seems it is possible to use getSSRProps so it should be do-able.

https://nuxt.com/docs/guide/directory-structure/plugins#vue-directives

LeSuisse avatar Mar 06 '23 21:03 LeSuisse

Nuxt 3 SSR support would be awesome.

jakubkoje avatar Jun 19 '23 00:06 jakubkoje

I have used this code to create Nuxt 3 server plugin. Place it in plugins/dompurify.server.ts file.

import { JSDOM } from "jsdom";
import createDOMPurify from "dompurify";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.directive("dompurify-html", {
    getSSRProps(binding) {
      const createDomPurifyInstance = () => {
        const window = new JSDOM("").window;
        return createDOMPurify(window);
      };
      const dompurifyInstance = createDomPurifyInstance();
      const innerHTML = dompurifyInstance.sanitize(binding.value);
      return {
        innerHTML,
      };
    },
  });
});

Note: I do not pass config/options/directive args to .sanitize function.

marcinkozaczyk avatar Jul 27 '23 15:07 marcinkozaczyk

Running into the same issue in Nuxt 3. It would indeed be very nice to get a Nuxt 3 plugin for this. I cannot get the example above by @marcinkozaczyk to work either. Elements using the directive just turn out empty after being server-side rendered, so I'm getting lots of hydration mismatches everywhere. If I add console.log(innerHTML) just before the return statement, I do see rendered HTML in the CLI, it's just not inserted into the element that uses the v-dompurify-html directive.

Might be related to this? https://github.com/vuejs/core/issues/8112

Anoesj avatar Dec 11 '23 16:12 Anoesj

how to use it in Nuxt3? Now, I use last version 5.0.0, but I don't know hot config it in defineNuxtPlugin.

freezyh avatar Dec 15 '23 06:12 freezyh

@osroca I made an example of using modules in nuxt2. hope this helps you. https://github.com/serialine/vue-dompurify-html/tree/example-nuxt2/examples/nuxt2 or #2257

How to use it in Nuxt3?

freezyh avatar Dec 15 '23 06:12 freezyh

Might be related to this? https://github.com/vuejs/core/issues/8112

I think you are correct. I am not sure what possibilities we have (at least while keeping the directive API approach) to manage the server side rendering if we cannot manipulate the DOM via getSSRProps.

In the meantime I added a bit of documentation and example to at least cover the client side part: https://github.com/LeSuisse/vue-dompurify-html/tree/main/packages/vue-dompurify-html#usage-with-nuxt-3

LeSuisse avatar Dec 15 '23 14:12 LeSuisse

Might be related to this? vuejs/core#8112

I think you are correct. I am not sure what possibilities we have (at least while keeping the directive API approach) to manage the server side rendering if we cannot manipulate the DOM via getSSRProps.

In the meantime I added a bit of documentation and example to at least cover the client side part: https://github.com/LeSuisse/vue-dompurify-html/tree/main/packages/vue-dompurify-html#usage-with-nuxt-3

Thanks, but It not works and show error tips, you can view the nuxt issues: https://github.com/nuxt/nuxt/issues/13382 so I add the code : // domPurify.server.ts `import VueDOMPurifyHTML from 'vue-dompurify-html'

export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.use(VueDOMPurifyHTML, {}) }) `

to solve the problem, It works!

but another problem appear, for example I will use highlight.js directive the sametime my code: 1.<div v-highlight v-html="text2"></div> 2.<div v-highlight v-dompurify-html="text2"></div> the one show normal, the two not normal , It will break hightlight.js structure, how to solve it! 微信图片_20231216151507

freezyh avatar Dec 16 '23 07:12 freezyh

I tried to be creative and solve this by parsing the sanitized HTML and supply it as childNodes in getSSRProps. That didn't work either :-( (I might add that I replaced the functionality in updateComponent to remove old childNodes and append new childNodes as well - still didn't help.)

filiphazardous avatar Apr 30 '24 11:04 filiphazardous